Skip to content

Add RFC 9110-aware idempotent HTTP client retry filter - #12640

Draft
scprek wants to merge 4 commits into
micronaut-projects:5.0.xfrom
scprek:idempotent-http-retry
Draft

Add RFC 9110-aware idempotent HTTP client retry filter#12640
scprek wants to merge 4 commits into
micronaut-projects:5.0.xfrom
scprek:idempotent-http-retry

Conversation

@scprek

@scprek scprek commented Apr 28, 2026

Copy link
Copy Markdown

Add RFC 9110-aware idempotent HTTP client retry filter

Summary

Introduces an opt-in client-side retry filter that automatically retries idempotent HTTP requests on transient failures, following RFC 9110. Disabled by default; zero behavior change for existing users.

micronaut.http.client.retry:
  enabled: true
  attempts: 3
  delay: 500ms
  multiplier: 1.5
  max-delay: 10s
  jitter: 0.25
  respect-retry-after: true
Rendered Adocs Screenshot 2026-04-28 at 5 49 32 PM Screenshot 2026-04-28 at 5 49 37 PM Screenshot 2026-04-28 at 5 49 41 PM

Out of scope (good follow-up candidates)

To keep this initial PR focused strictly on core reactive behavior and RFC compliance, the following items were intentionally deferred:

  • Per-service config (micronaut.http.services.<id>.retry.*) — V1 uses the global @Primary config.
  • Extracting shared exponential-backoff / jitter math to core so SimpleRetry (in retry) and IdempotentRetryClientFilter can share one implementation. The math is duplicated today; deduplicating in this PR would require depending on micronaut-retry, which defeats this filter's "no extra dependency" property.
  • Promoting RetryAfterParser to HttpHeaders.findRetryAfter().
  • Promoting IDEMPOTENCY_KEY_HEADER to HttpHeaders if/when the IETF draft is republished.
  • Adding RFC 9110 §15 category methods (isInformational(), isSuccess(), isRedirection(), isClientError(), isServerError()) to HttpStatus. These don't exist today and would be useful in many places beyond this filter.

Why

@Retryable already exists for declarative @Client interfaces, but it's a different tool with a real gap:

Concern @Retryable (AOP) This filter
Applies to programmatic HttpClient No (AOP needs an interface) Yes
Default predicate Type-based: retries any Exception (incl. HttpClientResponseException for 404) HTTP-aware: 5xx / 429 / 408 / transport errors
Method-aware (idempotency) No — retries POST same as GET Yes — RFC 9110 §9.2.2
Retry-After header No Yes (delta-seconds + HTTP-date)
Total-latency cap (SLA) None — N attempts × per-call timeout (latency multiplies with retries) Implicit — bounded by request-timeout
Idempotency-Key opt-in No Yes

The shared-vs-fresh budget is the key architectural consequence. For callers that need predictable p99 latency — payment gateways with strict SLAs, sync APIs feeding into a request-response chain — shared-clock is the desired model: the whole call cannot exceed request-timeout regardless of how many retries fire. A naive filter that retries every HttpClientException will, on a 404, churn through Mono.delay(1s + 2s + 4s + 8s) inside the same stream — and the upstream request-timeout window kills the stream with a misleading timeout instead of the original 404. Scoping retries to statuses that can recover keeps the budget for failures that justify spending it.

On service meshes: For in-mesh traffic, an Istio (or Linkerd) VirtualService retry policy provides the same total-latency-cap behavior as this filter's shared-clock model — the filter doesn't add SLA value there. Its value is for traffic that leaves the mesh (external APIs), deployments without a mesh (Lambda, ECS, App Runner, plain VMs), and Idempotency-Key-aware POST retry, which mesh proxies cannot do safely without application help.

The two complement each other: @Retryable for cross-cutting business logic (with fresh per-attempt timeouts), this filter for transport-level recovery on idempotent requests.

A note on Retry-After: HttpHeaders.RETRY_AFTER has existed as a name constant in http/src/main/java/io/micronaut/http/HttpHeaders.java since the early days, but a full-tree grep confirms nothing in Micronaut core, the HTTP client, the HTTP server, or any test parsed or honored the header. Netty doesn't either — it's a protocol library, not a retry orchestrator. This PR is the first place in the framework where the constant has operational meaning.

Related issues

This PR addresses long-standing community concerns that @Retryable is HTTP-agnostic and unsafe-by-default for HTTP clients. It does not modify @Retryable's behavior; instead it provides a complementary HTTP-aware filter that ships with the client core. Concrete linkage:

Maintainers: please decide whether either issue can be closed in light of this — the original concerns about @Retryable itself remain technically open, but the practical "give me a safe default for HTTP retry" outcome is now available.

What changes

New (http-client-core/src/main/java/io/micronaut/http/client/retry/):

  • HttpRequestRetryPredicate — public functional interface; default classifies via HttpMethod.isIdempotent() + Idempotency-Key header
  • HttpResponseRetryPredicate — public functional interface; default retries 5xx / 429 / 408 / transport errors
  • IdempotentRetryClientFilter@Internal @ClientFilter, opt-in via property
  • RetryAfterParser@Internal parser for delta-seconds + HTTP-date forms
  • DefaultRetryPredicateFactory@Internal factory providing replaceable defaults

Modified:

  • HttpClientConfiguration — added nested RetryConfiguration (mirrors Http2ClientConfiguration shape)
  • DefaultHttpClientConfiguration — wired DefaultRetryConfiguration
  • HttpMethod — added isSafe() and isIdempotent() instance methods (RFC 9110 §9.2.1 / §9.2.2)
  • clientRetry.adoc — opening paragraph now frames the two complementary retry mechanisms (@Retryable AOP advice vs. this filter) with cross-links to both, so a reader landing on either page can self-route
  • idempotentRetry.adoc — new top-level section under httpClient/ with linked RFC references, service-mesh interaction note, timeout-correlation guidance, and parallel Java/Kotlin example
  • toc.yml — registered the new section between clientFilter and clientHttp2

Tests:

  • HttpMethodSpec (new) — direct unit coverage for isSafe() / isIdempotent() over every enum constant.
  • HttpClientConfigurationSpec (new) — locks the RetryConfiguration contract: defaults match the published values, Toggleable defaults to disabled, setAttempts clamps via parametrized data table, and setDelay / setMaxDelay / setMultiplier / setJitter reject invalid inputs with IllegalArgumentException.
  • RetryAfterParserSpec (new) — delta-seconds, HTTP-date with fixed Clock, past-date coercion, malformed input, overflow.
  • HttpResponseRetryPredicateSpec (new) — full status-code matrix for isRetryableStatus(int) + default-predicate behavior across HttpClientResponseException, transport HttpClientException, and arbitrary throwables.
  • IdempotentRetryClientFilterSpec (new) — covers idempotent dispatch, terminal 4xx fast-fail, 5xx / 429 / 408 retry, transport errors, Retry-After (both forms with a fixed clock), respectRetryAfter: false, streamed-body bypass, IN_RETRY_LOOP cleanup on success / error / request-reuse, and enabled: false pass-through.
  • IdempotentRetryIntegrationSpec (new) — EmbeddedServer integration cases for retry-on-503, exhaustion, and POST non-retry.
  • CustomResponseRetryPredicate.java / .kt (new doc-example sources) + CustomResponseRetryPredicateSpec.java (new) — exercises the doc-example end-to-end with a real EmbeddedServer, asserting both the 425 retry path and the still-terminal-4xx path.

Design decisions

  1. Opt-in default. Zero behavior change for existing users.
  2. Two predicates, both replaceable via @Replaces. One axis decides "is this safe to retry," the other "did the failure warrant a retry." Splitting them lets users extend one without rebuilding the other.
  3. Idempotency by method, not interface annotation. New HttpMethod.isSafe() / isIdempotent() follow the existing requiresRequestBody() pattern; programmatic and declarative clients share semantics.
  4. Streamed-body bypass. Requests with a Publisher<?> body cannot be replayed without unbounded buffering. Documented.
  5. Filter re-entry guard. continuation.proceed() re-runs the filter chain (see MethodFilter.ReactiveContinuationImpl). An IN_RETRY_LOOP request attribute makes re-entries pass through; outermost invocation owns the retry loop. Captured in a code comment.
  6. No manual Netty buffer release. NettyHttpClient already releases the underlying ByteBuf in a finally block before propagating HttpClientResponseException. Calling release() here would no-op or throw IllegalReferenceCountException. Captured in a code comment so a Spring/raw-Reactor-Netty reader doesn't independently "fix" it.
  7. Filter ordered outermost (HIGHEST_PRECEDENCE + 100). Each retry triggers the entire downstream chain again, so auth filters re-issue fresh tokens and tracing filters create new spans per attempt.
  8. Per-service config (micronaut.http.services.<id>.retry.*) deliberately deferred. (See Out of Scope.)
  9. Idempotency-Key constant on the predicate, not HttpHeaders. The IETF draft (draft-ietf-httpapi-idempotency-key-header) has expired without publication; promoting it to HttpHeaders would imply IANA-registered status it doesn't have.
  10. RetryAfterParser kept @Internal. Could plausibly live as HttpHeaders.findRetryAfter() alongside findDate(), but only one consumer needs it today. Promotion is a one-commit refactor if a second consumer appears.

Mermaid Diagram

AI generated

sequenceDiagram
    autonumber
    actor Sub as Caller
    participant Filter as Retry Filter
    participant Outer as Outer Mono.defer
    participant Retry as retryWhen
    participant Inner as Inner Mono.defer
    participant Downstream as Downstream Filters
    participant Netty as NettyHttpClient

    Note over Filter: filter(request, continuation) called synchronously
    Filter->>Filter: shouldBypassRetry(request) - false (config OK, idempotent, replayable, not re-entry)
    Filter-->>Sub: return Mono.defer(...).doFinally(...)
    Note right of Filter: No request mutation yet -<br/>side effects deferred to subscription

    Sub->>Outer: subscribe()
    Outer->>Outer: Evaluate Outer Supplier
    Note right of Outer: setAttribute(IN_RETRY_LOOP, TRUE)<br/>AtomicLong attempted = 0<br/>(both per-subscription)
    Outer->>Retry: subscribe Inner.retryWhen(spec)
    Retry->>Inner: subscribe()
    Inner->>Inner: Evaluate Inner Supplier
    Inner->>Downstream: continuation.proceed()
    Downstream->>Netty: Execute attempt 1
    Netty-->>Downstream: 503 + ByteBuf released by NettyHttpClient
    Downstream-->>Inner: HttpClientResponseException
    Inner-->>Retry: error signal

    Note right of Retry: companion runs:<br/>attempted.getAndIncrement()<br/>shouldRetry == true
    Retry->>Retry: Mono.delay(backoff with jitter)

    Note over Retry, Inner: RETRY - re-subscribe Inner
    Retry->>Inner: resubscribe()
    Inner->>Inner: Evaluate Inner Supplier (fresh proceed)
    Inner->>Filter: continuation.proceed() re-enters chain

    Note over Filter: IN_RETRY_LOOP == TRUE on re-entry
    Filter->>Downstream: continuation.proceed() (bypass - no retry wrapper)
    Downstream->>Netty: Execute attempt 2
    Netty-->>Downstream: 200 OK
    Downstream-->>Inner: HttpResponse
    Inner-->>Retry: response signal (terminal)
    Retry-->>Outer: HttpResponse passes through

    Outer-->>Sub: HttpResponse
    Note over Outer: .doFinally(SignalType.ON_COMPLETE)<br/>removeAttribute(IN_RETRY_LOOP)
Loading

Compatibility

  • ./gradlew :micronaut-http-client-core:japiCmp clean — additions only, no binary breaks.
  • All RFC text references in Javadoc, code comments, and the user guide use clickable links.
  • Public API surface: 2 functional interfaces + 1 nested config class + 2 new instance methods on HttpMethod. All new types @since 5.0.0.

Verification

./gradlew :micronaut-http-client-core:test :micronaut-http-client:test cM spotlessCheck japiCmp docs — all green. New unit + integration specs cover: idempotent / non-idempotent dispatch, 4xx fast-fail, transport-error retry, exhaustion behavior, Retry-After (both delta-seconds and HTTP-date with a fixed Clock), streamed-body bypass, and disabled pass-through. No regressions in the existing :micronaut-http-client suite.

Copilot AI review requested due to automatic review settings April 28, 2026 22:08

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an opt-in, RFC 9110-aware HTTP client retry filter that retries only idempotent requests on transient failures, including Retry-After support, with accompanying configuration, docs, and tests.

Changes:

  • Introduces IdempotentRetryClientFilter plus request/response retry predicate extension points and Retry-After parsing.
  • Adds HttpMethod.isSafe() / isIdempotent() and client retry configuration wiring.
  • Updates docs TOC + retry docs, and adds unit/integration/spec coverage and doc-example tests.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test-suite/src/test/java/io/micronaut/docs/http/client/retry/CustomResponseRetryPredicateSpec.java Adds end-to-end doc-example test verifying a custom response retry predicate behavior.
test-suite/src/test/java/io/micronaut/docs/http/client/retry/CustomResponseRetryPredicate.java Provides Java doc-example showing how to replace the default response retry predicate.
test-suite-kotlin/src/test/kotlin/io/micronaut/docs/http/client/retry/CustomResponseRetryPredicate.kt Provides Kotlin doc-example equivalent of the custom response retry predicate.
src/main/docs/guide/toc.yml Registers the new idempotent retry documentation section in the guide TOC.
src/main/docs/guide/httpClient/idempotentRetry.adoc Documents the new retry filter, defaults, configuration, and timeout/mesh considerations.
src/main/docs/guide/httpClient/clientAnnotation/clientRetry.adoc Reframes client retry docs to distinguish @Retryable vs the new transport-level filter.
http/src/test/groovy/io/micronaut/http/HttpMethodSpec.groovy Adds coverage for HttpMethod.isSafe() and isIdempotent().
http/src/main/java/io/micronaut/http/HttpMethod.java Adds RFC 9110 semantics helpers: isSafe() and isIdempotent().
http-client/src/test/groovy/io/micronaut/http/client/retry/IdempotentRetryIntegrationSpec.groovy Adds EmbeddedServer integration tests for retry behavior across GET/POST and exhaustion.
http-client/src/test/groovy/io/micronaut/http/client/retry/IdempotentRetryClientFilterSpec.groovy Adds unit tests for filter retry logic, Retry-After, bypass paths, and disablement.
http-client-core/src/test/groovy/io/micronaut/http/client/retry/RetryAfterParserSpec.groovy Adds unit tests for parsing delta-seconds / HTTP-date Retry-After forms and edge cases.
http-client-core/src/test/groovy/io/micronaut/http/client/retry/HttpResponseRetryPredicateSpec.groovy Adds unit tests for default retryable status matrix and predicate behavior.
http-client-core/src/main/java/io/micronaut/http/client/retry/RetryAfterParser.java Implements internal Retry-After parsing per RFC 9110 §10.2.3.
http-client-core/src/main/java/io/micronaut/http/client/retry/IdempotentRetryClientFilter.java Implements the opt-in retry filter with exponential backoff/jitter and Retry-After handling.
http-client-core/src/main/java/io/micronaut/http/client/retry/HttpResponseRetryPredicate.java Adds public functional interface for response/exception-based retry decisions + defaults.
http-client-core/src/main/java/io/micronaut/http/client/retry/HttpRequestRetryPredicate.java Adds public functional interface for request idempotency eligibility + defaults.
http-client-core/src/main/java/io/micronaut/http/client/retry/DefaultRetryPredicateFactory.java Provides default predicate beans when users don’t supply replacements.
http-client-core/src/main/java/io/micronaut/http/client/HttpClientConfiguration.java Adds nested RetryConfiguration and new accessor for retry config.
http-client-core/src/main/java/io/micronaut/http/client/DefaultHttpClientConfiguration.java Wires default retry configuration into the default HTTP client configuration implementation.

Comment on lines +1000 to +1007
/**
* Obtains the idempotent retry configuration.
*
* @return The retry configuration, or {@code null} if retry is not configured.
* @since 5.0.0
*/
public HttpClientConfiguration.@Nullable RetryConfiguration getRetryConfiguration() {
return null;

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

The base HttpClientConfiguration#getRetryConfiguration() implementation always returns null, which makes the new API easy to misuse (callers must always null-check) and makes it impossible for callers to observe configured defaults unless they happen to have a concrete subtype override. Consider returning a non-null default RetryConfiguration instance from the base class (and letting configuration binding override it), aligning with other nested client config patterns and simplifying consumption.

Suggested change
/**
* Obtains the idempotent retry configuration.
*
* @return The retry configuration, or {@code null} if retry is not configured.
* @since 5.0.0
*/
public HttpClientConfiguration.@Nullable RetryConfiguration getRetryConfiguration() {
return null;
private final RetryConfiguration retryConfiguration = new RetryConfiguration();
/**
* Obtains the idempotent retry configuration.
*
* @return The retry configuration.
* @since 5.0.0
*/
public HttpClientConfiguration.RetryConfiguration getRetryConfiguration() {
return retryConfiguration;

Copilot uses AI. Check for mistakes.

@scprek scprek Apr 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back here. The null-at-base pattern is established for getHttp2Configuration()
and getWebSocketCompressionConfiguration() in this same file, and serves a specific
purpose: it lets user-defined HttpClientConfiguration subclasses (e.g.
ClientTwoHttpConfiguration in
http-client/src/test/groovy/io/micronaut/http/client/ClientSpecificLoggerSpec.groovy)
opt into individual nested configs without inheriting unrelated defaults. Switching
getRetryConfiguration() to a non-null base would silently inject a default-valued
RetryConfiguration into every existing custom subclass — that's an implicit behavior
change in user code, not an ergonomic win.

A second wrinkle specific to retry: this feature is disabled-by-default (intentional,
to keep zero behavior change for existing users). A non-null base default would mean
every custom subclass exposes a configured-but-disabled RetryConfiguration — a weirder
state than null, since the object exists but its other fields (delay, attempts,
multiplier …) only matter once you flip enabled = true somewhere else.

The @primary DefaultHttpClientConfiguration overrides to a non-null bound instance, so
the canonical access path the filter and ~all users hit never sees null. The null only
surfaces for custom subclasses that explicitly opted out of retry by not overriding —
which is the same explicit opt-in pattern they already use for HTTP/2 and WebSocket
compression.

If the team wants to revisit "should base nested-config getters return null or a
non-null default?" as a broader design question, I'd treat it as a separate refactor
that updates Http2 / WebSocketCompression / Retry consistently — happy to follow up on
that, but I don't want to ship a single inconsistent case in this PR.

Copilot AI 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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

@graemerocher graemerocher added the type: enhancement New feature or request label Apr 29, 2026
@graemerocher

Copy link
Copy Markdown
Contributor

Thanks, we are very late in the 5.0.x cycle so we will look at this for 5.1.x

@scprek

scprek commented Apr 29, 2026

Copy link
Copy Markdown
Author

Thanks, we are very late in the 5.0.x cycle so we will look at this for 5.1.x

I figured, and no rush. I'm still iterating a little on these copilot reviews anyhow

@scprek
scprek requested a review from Copilot April 29, 2026 14:16

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

return throwable -> {
if (throwable instanceof HttpClientResponseException ex) {
HttpResponse<?> response = ex.getResponse();
return isRetryableStatus(response.getStatus().getCode());

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

HttpResponseRetryPredicate.rfc9110() calls response.getStatus().getCode(), but HttpResponse#getStatus() delegates to HttpStatus.valueOf(code()) and will throw for custom/unknown HTTP status codes. That would turn an HTTP error into an unexpected IllegalArgumentException and break retry behavior. Use the numeric status (response.code() or ex.code()) when checking retryability instead of getStatus().

Suggested change
return isRetryableStatus(response.getStatus().getCode());
return isRetryableStatus(response.code());

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +227
HttpStatus status = ex.getStatus();
if (status != HttpStatus.TOO_MANY_REQUESTS && status != HttpStatus.SERVICE_UNAVAILABLE) {

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

retryAfterFromFailure uses ex.getStatus() (and compares to HttpStatus enum). HttpClientResponseException#getStatus() ultimately calls HttpResponse#getStatus() which can throw for custom/unknown status codes. To avoid unexpected IllegalArgumentException during delay computation, use the numeric status (ex.code() or ex.getResponse().code()) for the 429/503 checks.

Suggested change
HttpStatus status = ex.getStatus();
if (status != HttpStatus.TOO_MANY_REQUESTS && status != HttpStatus.SERVICE_UNAVAILABLE) {
int statusCode = ex.code();
if (statusCode != HttpStatus.TOO_MANY_REQUESTS.getCode() && statusCode != HttpStatus.SERVICE_UNAVAILABLE.getCode()) {

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +129
when: 'zero accepted (degenerate but coherent — every retry at base delay)'
cfg.multiplier = 0d

then:
cfg.multiplier == 0d

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

The comment describing multiplier = 0 as "every retry at base delay" is inaccurate for the current backoff math (delay * multiplier^retryIndex): with multiplier 0, the first retry uses the base delay but subsequent retries compute to 0ms. Either adjust the comment or disallow 0 if that behavior isn't intended.

Copilot uses AI. Check for mistakes.
@graemerocher graemerocher moved this from Backlog to In progress in 5.1.0 Release Jun 5, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in 5.1.0 Release Jun 5, 2026
@graemerocher graemerocher moved this from In progress to In review in 5.1.0 Release Jun 5, 2026
@graemerocher
graemerocher requested a review from yawkat June 5, 2026 18:15
*/
static HttpRequestRetryPredicate rfc9110() {
return request -> request.getMethod().isIdempotent()
|| request.getHeaders().contains(IDEMPOTENCY_KEY_HEADER);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is not technically part of RFC 9110

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

True, should be renamed


import java.time.Duration

class HttpClientConfigurationSpec extends Specification {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

kind of pointless tests tbh

@@ -0,0 +1,67 @@
Since Micronaut Framework 5.0, the HTTP client supports an opt-in transport-level retry filter that automatically retries idempotent requests on transient failures, following https://www.rfc-editor.org/rfc/rfc9110.html[RFC 9110]. The filter ships with the HTTP client core — no additional dependency is required, unlike ann:retry.annotation.Retryable[] which requires the `micronaut-retry` library.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

have to rewrite these for 5.1, also the @since tags

request.setAttribute(IN_RETRY_LOOP, Boolean.TRUE);
AtomicLong attempted = new AtomicLong();
return Mono.defer(() -> Mono.from(continuation.proceed()))
.retryWhen(buildRetrySpec(retries, attempted));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

subscribing to the same continuation multiple times is not well-supported, not sure this will work without issue

@yawkat

yawkat commented Jun 13, 2026

Copy link
Copy Markdown
Member

Honestly not sure if a filter is the right solution here

@scprek

scprek commented Jun 15, 2026

Copy link
Copy Markdown
Author

Honestly not sure if a filter is the right solution here

I know, but I couldn't figure out another way. So figured I'd just take a first attempt at something.

@graemerocher graemerocher moved this from In review to In progress in 5.1.0 Release Jun 15, 2026
@graemerocher
graemerocher marked this pull request as draft June 15, 2026 06:18
@sdelamo sdelamo removed this from 5.1.0 Release Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: enhancement New feature or request

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

5 participants