Skip to content

Do not retry requests with a consumed streaming body - #872

Open
Divyansh-db wants to merge 3 commits into
mainfrom
divyansh-vijayvergia_data/fix/no-retry-consumed-stream-body
Open

Do not retry requests with a consumed streaming body#872
Divyansh-db wants to merge 3 commits into
mainfrom
divyansh-vijayvergia_data/fix/no-retry-consumed-stream-body

Conversation

@Divyansh-db

Copy link
Copy Markdown
Contributor

Summary

Requests with a streaming body (e.g. files().upload()) are backed by a single-use
InputStream. When such a request received a retriable HTTP response (429/501/503), the
retry loop in ApiClient.executeInner re-sent the same InputStream. Because the first
attempt had already consumed the stream, the retry transmitted an empty body, which
manifested as either:

  • a 0-byte file written to the target despite an HTTP 204, or
  • a confusing downstream error (e.g. InternalError) surfaced from FilesImpl.upload.

This only occurs when a retriable status interrupts a streaming upload, so it is rare and
intermittent, but when it happens the upload silently corrupts data.

Root cause

Request holds the streaming body as a single InputStream reference. On retry,
executeInner reuses the same Request, and CommonsHttpClient wraps that already-consumed
stream in a fresh InputStreamEntity — sending 0 bytes. The upload PUT is non-idempotent,
and NonIdempotentRequestRetryStrategy treats 429/501/503 as retriable, so a transient 503
triggers the empty-body retry.

An InputStream-backed body cannot be rewound, so retrying is inherently unsafe once the body
has been sent.

Fix

In ApiClient.executeInner, skip the retry when the request has a streaming body and an
HTTP response was received (which proves the body was already transmitted), and surface the
original error to the caller so it can retry with a fresh stream.

The guard is deliberately narrow:

  • String / no-body requests (the vast majority of API calls) are repeatable — a new entity
    is built per attempt — so their retry behavior is unchanged.
  • Transport-level IOErrors (no response received, e.g. a pre-send ConnectException)
    still retry as before, since in that case the stream may not have been read yet.

The only behavior removed is the retry of an already-consumed stream, which never worked. This
lives in the hand-written core (ApiClient), so it applies to any streaming upload rather than
being specific to the Files API.

Tests

  • doesNotRetryStreamingBodyAfterResponse — a streaming PUT that gets a 503 now throws the
    typed TemporarilyUnavailable (503) instead of silently retrying with an empty body.
  • retriesNonStreamingBodyOn503 — a string-bodied request with the same 503 still retries and
    succeeds, confirming the guard does not regress ordinary requests.

Full module test suite passes (1404 tests, 0 failures).

When a request with a streaming body (e.g. Files.upload) receives a
retriable HTTP response, the SDK retried by re-sending the same
InputStream. The stream was already consumed by the first attempt, so
the retry transmitted an empty body — silently uploading a 0-byte file
(HTTP 204) or surfacing as a confusing InternalError.

InputStream-backed bodies cannot be rewound, so retrying is unsafe once
the body has been sent. Skip the retry when the request has a streaming
body and an HTTP response was received (which proves the body was
transmitted), and surface the original error so the caller can retry
with a fresh stream. Transport-level IOErrors (no response received,
e.g. a pre-send ConnectException) still retry, since the stream may not
have been read.
@github-actions

Copy link
Copy Markdown
Contributor

If integration tests don't run automatically, an authorized user can run them manually by following the instructions below:

Trigger:
go/deco-tests-run/sdk-java

Inputs:

  • PR number: 872
  • Commit SHA: a2b1c8d1ecfa028f107432f824716c08e499650e

Checks will be approved automatically on success.

@Divyansh-db
Divyansh-db deployed to test-trigger-is July 31, 2026 08:50 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant