Skip to content

Waiter.wait() does not validate WaiterConfig values #3674

@minjcho

Description

@minjcho

Describe the bug:

Waiter.wait() does not validate Delay and MaxAttempts values in WaiterConfig. Unlike API parameters (validated by ParamValidator and the AWS server), WaiterConfig is popped from kwargs and consumed locally with no validation at any layer.

This leads to Python runtime errors or silently broken behavior:

  • Delay: -1ValueError: sleep length must be non-negative
  • Delay: 0 → No sleep between polls, rapid-fire API calls
  • MaxAttempts: 0 or -1 → Polls once, then immediately raises WaiterError
  • MaxAttempts: 'abc'TypeError: '>=' not supported between instances of 'int' and 'str'

Other input types are validated properly:

Parameter type Invalid input Validated by
API params (MaxResults: -1) InvalidParameterValue AWS API server
Type mismatch (InstanceIds: [-1]) ParamValidationError botocore ParamValidator
CLI common (--generate-cli-skeleton invalid) ParamValidation argparse
Pagination (--max-items -1) Warning printed CLI (warning only)
WaiterConfig (Delay: -1) ValueError from time.sleep() None

Expected Behavior:

Waiter.wait() should raise a clear error when WaiterConfig contains invalid values, before the polling loop begins.

Current Behavior:

# Delay: -1 → Python runtime error
>>> waiter.wait(Bucket='my-bucket', WaiterConfig={'Delay': -1, 'MaxAttempts': 2})
ValueError: sleep length must be non-negative

# MaxAttempts: 'abc' → Python runtime error
>>> waiter.wait(Bucket='my-bucket', WaiterConfig={'MaxAttempts': 'abc'})
TypeError: '>=' not supported between instances of 'int' and 'str'

# Delay: 0 → no error, but no sleep between API calls
>>> waiter.wait(Bucket='my-bucket', WaiterConfig={'Delay': 0, 'MaxAttempts': 2})
# Two API calls fired back-to-back with no interval

# MaxAttempts: 0 → polls once, then fails immediately regardless of state
>>> waiter.wait(Bucket='my-bucket', WaiterConfig={'MaxAttempts': 0})
WaiterError: Waiter BucketExists failed: Max attempts exceeded

The relevant code in botocore/waiter.py:

config = kwargs.pop('WaiterConfig', {})
sleep_amount = config.get('Delay', self.config.delay)
max_attempts = config.get('MaxAttempts', self.config.max_attempts)
# No validation — values go directly into time.sleep() and >= comparison

Reproduction Steps:

import boto3

s3 = boto3.client('s3')
waiter = s3.get_waiter('bucket_exists')

# Case 1: Delay -1 → ValueError from time.sleep()
waiter.wait(Bucket='any-bucket', WaiterConfig={'Delay': -1, 'MaxAttempts': 2})

# Case 2: MaxAttempts 'abc' → TypeError from >= comparison
waiter.wait(Bucket='any-bucket', WaiterConfig={'Delay': 1, 'MaxAttempts': 'abc'})

# Case 3: Delay 0 → rapid-fire API calls (no error, but dangerous)
waiter.wait(Bucket='any-bucket', WaiterConfig={'Delay': 0, 'MaxAttempts': 3})

Possible Solution:

Add validation at the top of Waiter.wait(), matching the waiter JSON schema constraints already defined in tests/functional/test_waiter_config.py (delay >= 0, maxAttempts >= 1):

config = kwargs.pop('WaiterConfig', {})
sleep_amount = config.get('Delay', self.config.delay)
max_attempts = config.get('MaxAttempts', self.config.max_attempts)

if not isinstance(sleep_amount, (int, float)) or sleep_amount < 0:
    raise WaiterConfigError(
        error_msg=f'Invalid value for Delay: {sleep_amount!r}. Must be a non-negative number.'
    )
if not isinstance(max_attempts, int) or max_attempts < 1:
    raise WaiterConfigError(
        error_msg=f'Invalid value for MaxAttempts: {max_attempts!r}. Must be a positive integer.'
    )

Additional Information/Context:

This also affects AWS CLI, which recently added --delay and --max-attempts flags for wait commands (aws/aws-cli#10224). I've added CLI-layer validation there, but boto3 users calling waiter.wait() directly are still exposed.

SDK version used: botocore 1.35.99

Environment details: macOS Darwin 25.4.0, Python 3.12.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions