Add a per-request retry-on-exception hook to HxClient (lib-httpx 2.6.0) [release] - #117
Conversation
retryCondition is a Predicate<Throwable>, so it answers identically for every request. That is not enough for a caller with non-idempotent endpoints: a retry is safe when the request is idempotent OR when it provably never reached the server, and only the first is visible in the request. A bare HttpTimeoutException forces the distinction — it says the response never arrived, not that the request never did — so the same exception warrants opposite answers for a GET and a POST. Add shouldRetryOnException(HttpRequest, Throwable) and make it the hook the retry policy consults, on both the sync and async paths. The policy is already built per call inside sendWithRetry(request, ...), so the request is in scope and no thread-local plumbing is needed. The existing shouldRetryOnException(Throwable) becomes its default implementation, so current overrides and any configured retryCondition keep working unchanged. No new hook for the response side: shouldRetryOnResponse(HttpResponse) can already reach the request via HttpResponse.request(), which is what lets a subclass also vary the retryable status codes by method — relevant for a 502, returned after the request was forwarded, versus a 503, returned when there was nothing to forward it to. Three tests, each verified to fail when the hook is unwired: a timed-out GET re-sent 3 times where the default condition stops at 1, a reset POST delivered once where the default makes 3 attempts, and the same on the async path built through the wrap-a-built-client recipe the README documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
- README: bump the dependency coordinate to 2.6.0, so the snippet matches the released VERSION - README: note that the wrap recipe does not carry over a custom tokenStore, and point at the three-arg constructor that does - HxClient: clarify in the @PARAM that the hook sees the pre-auth request, so a subclass keys off method and URI rather than headers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude check and review this PR |
|
Claude finished @pditommaso's task in 2m 13s —— View job Review: per-request retry-on-exception hook
Verdict: looks good — no correctness, security or compatibility problem found. The change is additive and narrowly scoped: one new overload, both retry sites rewired to it, everything else untouched. What I checkedBoth retry paths are covered. No shared-state hazard. The Backward compatibility holds. The default implementation delegates to Overload resolution is clean. The old Release hygiene is consistent. Docs. The javadoc and README both call out the one real footgun: overriding the new 2-arg hook bypasses a configured Non-blocking notes
Not verifiedI could not run I'm not able to submit a formal GitHub approval — this action posts review feedback as a comment only. Treat the above as an approving review; see the FAQ for the formal-review workaround. |
- Add a spec pinning down the composition the javadoc promises: a subclass whose 2-arg override calls super on top of a configured retryCondition. The GET half proves super reaches the configured condition (3 attempts, where the default condition would stop at 1); the POST half proves the per-request veto still wins. - HxClient: warn on the 1-arg hook that an override must not delegate to the 2-arg form, since the 2-arg default calls back into this one and would recurse without end. - README: note that the exception hook sees the request as built while HttpResponse.request() is the request as sent, so the two request-aware hooks are not exact mirror images. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Why
HxConfig.retryConditionis aPredicate<Throwable>, so it answers the same way for every request. That is not enough for a caller with non-idempotent endpoints: a retry is safe when the request is idempotent or when it provably never reached the server, and only the first of those is visible in the request.A bare
HttpTimeoutExceptionis the case that forces the distinction — it means the response never arrived, not that the request never did, so the same exception warrants opposite answers for aGETand aPOST. APredicate<Throwable>cannot express that on its own.What
HxClient.shouldRetryOnException(HttpRequest, Throwable)— the hook the retry policy now consults on both the sync and async send paths.shouldRetryOnException(Throwable)becomes its default implementation, so current overrides and any configuredHxConfig.retryConditionkeep working unchanged.HttpResponse.request().Compatibility
No API break. The new hook delegates to the old one by default, and a search across platform, wave, nextflow and sched found no consumer overriding
shouldRetryOnException(Throwable), so the added indirection is invisible to existing callers.Note that the 2-arg default calling into the overridable 1-arg method is load-bearing, not incidental: it is the mechanism by which a legacy override still takes effect. Restructuring it to bypass the 1-arg method via a private helper was tried and rejected — it makes
should honour a shouldRetryOnException override from a subclassfail, because the retry sites call the 2-arg form. The flip side is that an override of the 1-arg method must not delegate back to the 2-arg form, which would recurse; that is now called out in the 1-arg javadoc.Release
Bumps
lib-httpxto 2.6.0 —VERSION,changelog.txtand the README dependency coordinate are updated together.Test plan
./gradlew :lib-httpx:testpasses — 22 specs inHxClientRetryIntegrationTest. Four are new:HttpTimeoutExceptionthat the default condition gives up on: 3 attempts where the default yields 1.CONNECTION_RESET_BY_PEERthat the default condition would retry: 1 attempt where the default yields 3.sendAsync, confirming bothRetryableconstruction sites were rewired.retryCondition— a subclass that vetoes POSTs but defers tosuperfor everything else, on top of aretryConditionthat retries request timeouts. The GET half provessuperreaches the configured condition (3 attempts; the default condition alone would stop at 1), the POST half proves the per-request veto still wins.Backward-compatible delegation of an existing 1-arg override is covered by the pre-existing
should honour a shouldRetryOnException override from a subclassspec, which is unchanged and still passes.🤖 Generated with Claude Code