Skip to content

fix: defer stream context cancel to iterator cleanup#688

Open
adek05 wants to merge 2 commits intogoogleapis:mainfrom
radishbuild:fix/stream-timeout-cancel
Open

fix: defer stream context cancel to iterator cleanup#688
adek05 wants to merge 2 commits intogoogleapis:mainfrom
radishbuild:fix/stream-timeout-cancel

Conversation

@adek05
Copy link

@adek05 adek05 commented Feb 7, 2026

Summary

When HTTPOptions.Timeout is set, sendStreamRequest creates a timeout context and immediately defers its cancellation. Since sendStreamRequest returns before the caller iterates the stream, the deferred cancel() fires while chunks are still being received, causing resp.Body.Read() to fail with "context canceled" after the first buffered chunk.

This means any streaming request with a timeout set loses all chunks except the first one.

Root Cause

// In sendStreamRequest (before fix):
requestContext, cancel = context.WithTimeout(ctx, *timeout)
defer cancel()  // ← fires when sendStreamRequest returns, NOT when iteration completes

BEFORE (broken):

sendStreamRequest returns → defer cancel() fires → context dead
iterator starts → Scan() gets chunk 1 (buffered) → Scan() fails on chunk 2

AFTER (fixed):

sendStreamRequest returns → cancel stored in responseStream
iterator starts → reads all chunks → defer cleanup calls cancel() + Close()

Test Plan:
Added TestSendStreamRequestTimeoutNotCancelledEarly: httptest server sends 4 SSE chunks with 10ms delays between flushes, 5s timeout. Asserts all 4 chunks are received.
Verified the test fails without the fix (only receives chunk 1)

sendStreamRequest defers cancel() on the timeout context, which fires
when the function returns — before the iterator consumes any chunks.
This causes the HTTP connection to be torn down after the first
buffered chunk, silently dropping subsequent SSE events.

The test sends 4 SSE chunks with 10ms delays between flushes and a 5s
timeout. Without the fix only chunk 1 is received.
sendStreamRequest created a timeout context and deferred cancel(),
which ran when the function returned — before the iterator had consumed
any SSE chunks. This cancelled the request context and tore down the
HTTP connection, causing multi-chunk streaming responses to be silently
truncated after the first buffered event.

Move the cancel function into responseStream so it is called when the
iterator completes, alongside resp.Body.Close(). On error paths in
sendStreamRequest the cancel is called immediately.
@google-cla
Copy link

google-cla bot commented Feb 7, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@adek05
Copy link
Author

adek05 commented Feb 11, 2026

@Sivasankaran25 hey, is there any process of getting this addressed or PRs merged or is this just a parking lot.

@Sivasankaran25
Copy link
Collaborator

Sivasankaran25 commented Feb 11, 2026

@Sivasankaran25 hey, is there any process of getting this addressed or PRs merged or is this just a parking lot.

Thanks you for your contributing. This PR has been filed internally, and reviews are in progress

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api:gemini-api Issues related to Gemini API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants