Skip to content

feat: server-driven and observable retry controls#92

Merged
OmarAlJarrah merged 2 commits into
mainfrom
feat/retry-controls
Jun 16, 2026
Merged

feat: server-driven and observable retry controls#92
OmarAlJarrah merged 2 commits into
mainfrom
feat/retry-controls

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

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-After values 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 staged DefaultRetryStep deliberately 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)

ServerOverrideRetryPredicate implements the existing composable retry-condition seam and reads a configurable response header (default X-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

… 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.
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.
@OmarAlJarrah OmarAlJarrah merged commit b6ccdc2 into main Jun 16, 2026
1 check passed
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.

Optional per-attempt retry-count request header Parse fractional Retry-After values Ship an opt-in server-driven retry predicate (X-Should-Retry)

1 participant