Catch urllib3 ProtocolError on streamed responses - #10606
Open
jinzhao1994 wants to merge 1 commit into
Open
Conversation
A urllib3 ProtocolError raised while reading a response body was not wrapped, so it propagated as-is out of StreamingBody.read(). It matched none of the entries in S3_RETRYABLE_DOWNLOAD_ERRORS, so s3transfer's part-level retry never engaged and a single mid-transfer connection reset failed the whole file download. This backports two upstream fixes that the v2 branch missed: * boto/botocore#2573 wraps URLLib3ProtocolError in a new ResponseStreamingError. * boto/s3transfer#301 adds ResponseStreamingError to S3_RETRYABLE_DOWNLOAD_ERRORS. Both halves are required: wrapping alone still would not match the retryable tuple.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When
aws s3 cp --recursivedownloads large objects, a single mid-transfer TCPreset fails the entire file. s3transfer's part-level retry
(
num_download_attempts, default 5) never engages.Production traceback (
aws-cli/2.36.5 Python/3.14.6 Linux, ~114 GB across 51objects,
max_concurrent_requests = 32):In that run, 4
ProtocolErrors mapped 1:1 onto 4download failedlines — noretry was attempted for any of them. From the same
--debuglog:Note the request-level retry in botocore cannot help here: the reset happens
while streaming the body, long after a
200response head was received, so thelog shows
botocore.retries.standard - Not retrying request.before eachfailure.
Root cause
urllib3.exceptions.ProtocolErrorinheritsProtocolError -> HTTPError -> Exception. It is neither aConnectionErrornor aResponseStreamingError, soexcept S3_RETRYABLE_DOWNLOAD_ERRORSinGetObjectTask._maindoes not match andthe exception escapes the retry loop entirely.
The
v2branch is missing two upstream fixes:a5e5119b6, 2021-12-10)URLLib3ProtocolErrorinto a newResponseStreamingError1.23.245fc308a11, 2024-03-06)ResponseStreamingErrortoS3_RETRYABLE_DOWNLOAD_ERRORS0.10.1awscliv1 has had both for a while (it currently resolves botocore 1.43.62 /s3transfer 0.19.2), so v2 is strictly behind v1 on download resilience here.
Both halves are needed — wrapping alone still would not match the retryable
tuple.
Change
awscli/botocore/exceptions.py: addResponseStreamingError(verbatim from cloudformation deploy: Use parameter default value only on stack creation #2573).awscli/botocore/response.py:StreamingBody.read()wrapsURLLib3ProtocolError(verbatim from cloudformation deploy: Use parameter default value only on stack creation #2573).awscli/s3transfer/utils.py: addResponseStreamingErrortoS3_RETRYABLE_DOWNLOAD_ERRORS(from Fix retrying incomplete downloads with urllib3 2.x boto/s3transfer#301).GetObjectTasktest asserting thepart-level retry now actually engages and the download completes.
StreamingBodyonv2has noreadinto(), so unlike current botocoredevelopthere is no second call site to guard.Scope
This does not address why the resets happen — that is a separate network-side
issue. It fixes the amplifier: a transient reset becomes a retryable, recoverable
part failure instead of a whole-file failure.
Testing
tests/unit— 9392 passed (one pre-existing unrelated failure intests/unit/customizations/history/test_history.py, present on a cleanv2).tests/functional/s3,tests/functional/s3transfer— 509 passed.ruff/ruff-format(pinned 0.4.8, per.pre-commit-config.yaml) clean.Generated by AI tools, and reviewed by Sheng Cao.