Fix/download non 2xx status#27256
Draft
torreypayne wants to merge 3 commits into
Draft
Conversation
Upgrade addressable to ~> 2.9 to mitigate vulnerability GHSA-h27x-rffw-24p4. Fixes: #26054
Prevents error response bodies (e.g. 503 Service Unavailable or 4xx error payloads) from being written into destination file streams during streaming downloads. Restores original pre-v1.0.0 allowlist filtering behavior (`!(200..299).include?(status)`) to prevent corrupted download offsets on retryable errors. Fixes googleapis/google-cloud-ruby#34750
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.
Historical Context & Root Cause
Prior to
google-apis-core v1.0.0(whenhttpclientwas used as the backend),DownloadCommandsafeguarded download streams by strictly checking HTTP status codes against an allowlist of successful download statuses:On July 31, 2025, in commit
cdf6281c733(PR #23524, migrating the underlying HTTP library fromhttpclienttoFaradayin v1.0.0),OK_STATUSwas deprecated (# @deprecated No longer used), and the condition was replaced with:When using Faraday's redirect middleware, Faraday invokes
on_datafor intermediate301/302redirect responses before following the redirect to the final URL. The checkstatus >= 300 && status < 400was introduced to prevent redirect HTML pages from being written to@download_io.However, in commit
69490b7b0f9(Sep 9, 2025), an explanatory comment was added above this line:This comment reveals the assumption that led to this bug: maintainers assumed Faraday only invokes
on_datafor successful2xxresponses. In reality, standard Faraday adapters (such asNet::HTTP) stream response bodies for all HTTP status codes—including4xxclient errors and5xxserver error payloads—directly throughon_data.Because
status >= 300 && status < 400only filters out3xxredirects, any streaming download that encounters an HTTP error (such as503 Service Unavailable) writes the error response body directly into@download_ioand advances@offsetby the length of the error message payload.Fixes googleapis/google-cloud-ruby#34750
Fix Implementation
In both
DownloadCommand(lib/google/apis/core/download.rb) andStorageDownloadCommand(lib/google/apis/core/storage_download.rb), modify theon_datacallback filter to skip any non-success status codes (!(200..299).include?(status)):This change directly implements the resolution proposed in issue #34750 and restores the original pre-v1.0.0 allowlist logic (
OK_STATUS) that protected download streams from error payload corruption.Downstream Impact & Execution Flow (Issue #34750)
In downstream clients like
google-cloud-storage, writing error payloads and advancing@offsetcaused consecutive retryable server errors (503 Service Unavailable) to request out-of-bounds byte ranges, resulting in fatal416 Range Not Satisfiableerrors or CRC32C checksum verification failures.Specifically, this caused consistent presubmit conformance test failures for
storage.objects.get:ConformanceTest#test_1-return-503_return-503-storage.objects.get-0ConformanceTest#test_1-return-reset-connection_return-503-storage.objects.get-0Step-by-Step Execution Flow of the Bug
When downloading an object (
object.download/:download_file):503 Service Unavailablewith a response body describing the error (e.g., 84 bytes).on_dataCallback: Because503is not in300...400,on_datawrites the 84-byte error message into@download_ioand increments@offsetfrom0to84.check_statusinspects503and raisesGoogle::Apis::ServerError(retryable).Retriablecatches the error and retries. Because@offsetis now84, the client assumes it is resuming a partially completed download and adds:Range: bytes=84-503again (84 bytes).on_datawrites the second error body and increments@offsetby another 84 bytes (168).Range: bytes=168-416 Range Not Satisfiable: Because the test file is 12 bytes ("My test file"), requesting a range starting at168is out of bounds. The server responds with416 Requested Range Not Satisfiable.416maps toGoogle::Apis::ClientError(400 "Invalid request"), which is non-retryable—aborting execution and failing the test.Risk Assessment
In HTTP/1.1 and HTTP/2 media downloads, there is zero valid use case where a client should write the response body of a non-2xx status code into a destination file stream (
@download_io):304 Not Modified) or redirect HTML (<a href="...">Moved</a>).Restricting chunk writing strictly to
200..299carries zero regression risk for upstream gems or services.