feat: server-driven and observable retry controls#92
Merged
Conversation
… and server-driven retries Three additive, opt-in enhancements to the retry layer: - Parse fractional Retry-After values. RetryAfterParser now reads the Retry-After delta-seconds form with toDoubleOrNull, so values like `Retry-After: 1.5` are honoured to nanosecond resolution instead of being silently ignored. Integer seconds and the HTTP-date form are unchanged, and the negative / NaN / non-finite guards plus the 365-day ceiling still apply. The numeric form now shares that ceiling with the date and Unix-epoch forms, so a far-future delta clamps rather than flowing through unbounded. Both retry layers benefit, since they delegate parsing here. - Optional per-attempt retry-count request header. RetrySettings gains an opt-in attemptHeaderName (default null). When set, the recovery-aware RetryStep stamps that header with the 1-based attempt ordinal on a per-attempt copy of the request, so servers and proxies can observe the retry count. The header is applied via Request.newBuilder, leaving the immutable template and any caller-supplied idempotency key untouched. - Server-driven retry predicate. New ServerOverrideRetryPredicate implements the composable HttpRetryConditionPredicate seam and honours an X-Should-Retry-style response header (configurable name): a truthy value forces a retry, a falsy value suppresses one, and an absent or unrecognised value defers to a delegate predicate. It is not installed by default; the default retryable-status set is unchanged, so 409 stays out of it unless a server explicitly opts a response in.
This was referenced Jun 16, 2026
The fractional Retry-After support switched the numeric branch to toDoubleOrNull(), which accepts the Java floating-point literal grammar: "30d" parses to 30.0 and "0x1p4" to 16.0. A header like `Retry-After: 30d` would therefore silently honour a 30-second delta instead of falling through to the backoff schedule. Screen the value against ^\d+(\.\d+)?$ before parsing so only the RFC 7231 delta-seconds form and the fractional extension real servers emit are honoured; anything else (type suffixes, hex-float, signs, exponents) falls through to the HTTP-date branch and ultimately the backoff schedule. Add parser tests for the rejected forms. Expand the ServerOverrideRetryPredicate tests to cover the full truthy/falsy token matrix and the unrecognised-token defer branch, and add a case proving a forced retry still does not re-send a non-replayable body. Clarify the predicate KDoc that the override flips classification only and does not bypass the attempt cap or the replayability gate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three additive, opt-in retry capabilities. Defaults are unchanged — existing behavior is preserved unless a caller enables a feature.
Fractional
Retry-After(#43)Retry-Aftervalues with a fractional component (e.g.1.5) are now parsed to sub-second delays, alongside the integer-seconds and HTTP-date forms. The existing 365-day ceiling now applies uniformly to the numeric form too (it previously clamped only the date/epoch forms), closing an inconsistency the parser's own contract already described.Per-attempt retry-count request header (#44)
When a header name is configured on the retry settings, each attempt carries its 1-based ordinal (original send = 1, first retry = 2, …) so servers and proxies can observe retries. The header is absent when unconfigured, and the per-attempt copy never mutates the immutable request template or its idempotency key. Implemented in the recovery-aware
RetryStep, which owns the per-attempt request; the stagedDefaultRetryStepdeliberately re-sends the in-flight pipeline request unmodified per its documented invariant, so this is scoped to the former.Opt-in server-driven retry predicate (#42)
ServerOverrideRetryPredicateimplements the existing composable retry-condition seam and reads a configurable response header (defaultX-Should-Retry): a truthy value forces a retry, a falsy value suppresses one, and an absent/unrecognized value defers to the wrapped predicate (which preserves stock status/exception classification). Off unless a caller installs it.Each capability has new tests. The new public options are reflected in the API snapshot.
Closes #43
Closes #44
Closes #42