Context
The retry transport in internal/api/retry.go mishandles response bodies in two ways and only partially parses Retry-After, causing connection leaks and a potential read-after-close for callers.
Evidence
All in internal/api/retry.go (retryTransport.RoundTrip):
- Body leak on 429 retry — when a 429 has a parsable
Retry-After, the code sleeps and continues before reaching the resp.Body.Close() below, so the previous response body is never closed:
if resp != nil && resp.StatusCode == 429 {
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := time.ParseDuration(retryAfter + "s"); parseErr == nil {
time.Sleep(seconds)
continue // <- skips the Body.Close() below
}
}
}
// Close response body if exists
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
-
Closed body returned on final failure — after the last attempt, the loop has already closed resp.Body, yet return resp, err hands that response to the caller, who will read from a closed body.
-
Retry-After HTTP-date form unhandled — only the delta-seconds form is parsed; RFC 9110 also allows an HTTP-date, which currently falls through to generic backoff.
Suggested fix
- Close the previous body before every retry path (including the 429
continue).
- On the final attempt, return the response without closing its body (or drain/close and return a synthesized error).
- Parse
Retry-After as either delta-seconds or HTTP-date (http.ParseTime).
- Add unit tests with a stub
RoundTripper covering 429-with-Retry-After, exhausted retries, and body lifecycle.
Context
The retry transport in
internal/api/retry.gomishandles response bodies in two ways and only partially parsesRetry-After, causing connection leaks and a potential read-after-close for callers.Evidence
All in
internal/api/retry.go(retryTransport.RoundTrip):Retry-After, the code sleeps andcontinues before reaching theresp.Body.Close()below, so the previous response body is never closed:Closed body returned on final failure — after the last attempt, the loop has already closed
resp.Body, yetreturn resp, errhands that response to the caller, who will read from a closed body.Retry-AfterHTTP-date form unhandled — only the delta-seconds form is parsed; RFC 9110 also allows an HTTP-date, which currently falls through to generic backoff.Suggested fix
continue).Retry-Afteras either delta-seconds or HTTP-date (http.ParseTime).RoundTrippercovering 429-with-Retry-After, exhausted retries, and body lifecycle.