Repository navigation
fix: Honor http.Client.Timeout in roundTripWithOptionalFollowRedirect - #4688
sushant-me wants to merge 3 commits into
Conversation
Fixes google#4584. roundTripWithOptionalFollowRedirect calls the configured RoundTripper directly so it can inspect a 301/302 instead of following or surfacing it. That is the whole point of the helper, but it also skips http.Client.Do - and http.Client.Do is where http.Client.Timeout is applied. A caller using WithTimeout therefore got a request that the timeout did not bound, while the same request through the client's ordinary path returned context.DeadlineExceeded. The deadline is now attached once, around the whole helper rather than per hop, so it spans redirects and body reads the way http.Client.Timeout does. On the success path the cancel func moves to a body wrapper and is released when the caller closes the body (the helper already documents that closing as the caller's job); every other exit releases it locally, which is also what go vet flagged in the first draft - the timer leaked when a transport returned a nil body. Two regression tests, both failing on master and passing here: * a RoundTripper that waits on req.Context().Done() plus a 20ms client timeout now returns context.DeadlineExceeded instead of waiting out the caller's context. The transport also has a 2s fallback return, so a regression fails the assertion rather than hanging the suite. * a redirect chain asserts that every hop sees the same deadline, which is what "spans the whole request" means; a per-hop timeout would give each call its own (the test compares the two deadlines). `go test ./...` and `go vet ./github` are clean.
|
The |
http.Client.Timeout in roundTripWithOptionalFollowRedirect
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4688 +/- ##
==========================================
- Coverage 98.64% 98.63% -0.01%
==========================================
Files 202 202
Lines 19004 19022 +18
==========================================
+ Hits 18746 18763 +17
- Misses 258 259 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
golangci-lint (unparam) reports that opts always receives nil in roundTripWithOptionalFollowRedirect (github.go:2285). Every caller passes no request options and the helper only forwards them, so the variadic is removed from both helpers and from the NewRequest call.
The successful-response path is the one case the existing tests do not reach: the deadline has to outlive the call so the caller can read the body, and Close has to release it. The new test pins both halves against the request context the transport actually saw.
|
Two commits pushed for the two red checks; both are in
Verified locally: |
|
Hi @sushant-me, thanks for the fix! #4585 by @sb123sb123 addresses the same issue (#4584), but it has been blocked on the Google CLA since 2026-10-01. Could you add |
|
Added |
Fixes #4584.
Closes: #4585 — the earlier attempt at the same issue, which has been blocked on the Google CLA since 2026-10-01.
roundTripWithOptionalFollowRedirectcalls the configuredRoundTripperdirectly so it can inspect a 301/302 instead of following or surfacing it. That is the point of the helper, but it also skipshttp.Client.Do— andhttp.Client.Dois wherehttp.Client.Timeoutis applied. A caller usingWithTimeouttherefore got a request the timeout did not bound, while the same request through the client's ordinary path returnedcontext.DeadlineExceeded.Fix
The deadline is attached once, around the whole helper rather than per hop, so it spans redirects and body reads the way
http.Client.Timeoutdoes:The deadline must outlive the call so the caller can read the body, so the cancel func moves to a body wrapper on the success path and is released when the body is closed — which the helper already documents as the caller's responsibility. Every other exit releases it locally. The first draft called
cancel()directly on the error path only;go vet(lostcancel) correctly flagged that the timer leaked when a transport returned a nil body, hence the structure above.Tests
Both fail on
masterand pass here:TestRoundTripWithOptionalFollowRedirect_HonorsClientTimeout— aRoundTripperthat waits onreq.Context().Done()with a 20 ms client timeout now returnscontext.DeadlineExceeded. The transport has a 2 s fallback return so a regression fails the assertion instead of hanging the suite.TestRoundTripWithOptionalFollowRedirect_TimeoutCoversAllRedirects— a redirect chain asserts every hop sees the same deadline, which is what "spans the whole request" means; a per-hop timeout would give each call a fresh one.go test ./...andgo vet ./githubare clean. No cross-host or same-host redirect behaviour changed — the existing tests for those still pass.