Skip to content

fix(go): return the response when the deadline is shorter than the retry backoff - #17521

Open
adidavid014 wants to merge 1 commit into
mainfrom
go-retrier-deadline-followup
Open

fix(go): return the response when the deadline is shorter than the retry backoff#17521
adidavid014 wants to merge 1 commit into
mainfrom
go-retrier-deadline-followup

Conversation

@adidavid014

@adidavid014 adidavid014 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Description

Follow-up to #17498, requested by name.com as the optional item in their report.

When a retryable response arrived and the request context's deadline would elapse before the computed backoff finished, the retrier slept anyway and the caller received context deadline exceeded — discarding the response that explained the failure. With Retry-After honored up to maxRetryDelay (60s), a single 429 became an opaque timeout.

The retrier now compares the remaining deadline against the delay and returns the response instead of sleeping:

delay, err := r.retryDelay(response, retryAttempt)
if err != nil {
    _ = response.Body.Close()
    return nil, err
}

// If the context's deadline would elapse before the backoff completes, return
// the response instead of sleeping through it.
if deadline, ok := request.Context().Deadline(); ok && time.Until(deadline) < delay {
    return response, nil
}

defer func() { _ = response.Body.Close() }()

The defer response.Body.Close() moves below the early return — otherwise the caller gets a closed body.

Behavior change worth a close look

This is user-visible, not a pure bugfix. Code doing errors.Is(err, context.DeadlineExceeded) on a rate-limited call now gets a *core.APIError with StatusCode: 429 instead. That is what was asked for and it is strictly more informative, but it is a change in what callers observe.

It also flips a test added in #17498. TestRetryWaitIsInterruptedByContext used WithTimeout(100ms) against Retry-After: 5 and asserted context.DeadlineExceeded — exactly the case this change now resolves to a 429. I treated the two behaviors as complementary rather than competing:

  • deadline too short for the backoff → return the response (new)
  • explicit cancel() during the backoff → return ctx.Err() (unchanged; still reachable when there is no deadline, which is the Ctrl-C case sleepWithContext exists for)

So that test is rewritten to use context.WithCancel, which is what it was actually about.

One deliberate omission: the comparison is a bare time.Until(deadline) < delay with no margin for the request itself. If the remaining time only slightly exceeds the delay, we still sleep and the next attempt likely dies on the deadline. I matched the reporter's suggestion exactly rather than pick a margin size — happy to add one if reviewers prefer.

Changes

  • generators/go-v2/base/src/asIs/internal/retrier.go_: deadline-vs-backoff comparison; Body.Close() reordered.
  • generators/go-v2/base/src/asIs/internal/retrier_test.go_: TestRetryWaitIsInterruptedByContext rewritten to cover cancellation; added TestRetryReturnsResponseWhenDeadlineShorterThanBackoff and TestRetryProceedsWhenDeadlineLongerThanBackoff.
  • Go SDK changelog entry.
  • Regenerated seed/go-sdk/**/internal/retrier{,_test}.go (366 files).

Testing

Built the generator from this branch and generated name.com's real SDK from their spec and generators.yml. Their original repro now returns 429: {"message":"slow down"} in ~3ms, where 1.57.9 returns context deadline exceeded after 2s.

  • All 8 retrier test functions pass as generated, including the 2 new ones.
  • go build ./... and go vet ./... clean.
  • Full-suite failure profile identical to 1.57.9 (only WireMock tests needing a local server).
  • Seed fixtures exhaustive/no-custom-config, imdb/no-custom-config, idempotency-headers build and pass.

Note on the snapshots: I patched them mechanically rather than running seed, since these are asIs files whose only per-fixture variation is the module path (verified: all 183 shouldRetry bodies identical, all 366 files gofmt-clean). I validated the shortcut by diffing a patched snapshot against genuine generator output — byte-identical modulo the module path. Worth re-running pnpm seed if you would rather not rely on that.

Generated with Claude Code


Open in Devin Review

…try backoff

When a retryable response arrived and the request context's deadline would
elapse before the computed backoff finished, the retrier slept anyway and the
caller received `context deadline exceeded` — discarding the response that
explained the failure. With `Retry-After` honored up to `maxRetryDelay` (60s),
a single 429 could turn into an opaque timeout.

The retrier now compares the remaining deadline against the delay and returns
the response instead of sleeping, so the caller sees the actual 429 (with its
headers and body). An explicit `cancel()` during the backoff still returns the
context's error, which is the case `sleepWithContext` exists for.

`TestRetryWaitIsInterruptedByContext` previously asserted
`context.DeadlineExceeded` for a short-deadline case that this change now
resolves to a 429, so it is rewritten to cover cancellation (its actual
subject) and two tests are added for the deadline-vs-backoff comparison.

Reported by name.com as the optional follow-up to #17498.

Co-Authored-By: Claude <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@github-actions

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-25T04:12:59Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
go-sdk square 136s (n=5) 285s (n=5) 117s -19s (-14.0%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-08-25T04:12:59Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-25 16:24 UTC

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.

2 participants