Skip to content

Fix the WebClient plugins' NPE when writeTo runs before the exit spanis created - #834

Merged
wu-sheng merged 2 commits into
apache:mainfrom
eye-gu:fix/13589-webclient-bodyinserter-npe
Sep 22, 2026
Merged

wu-sheng merged 2 commits into
apache:mainfrom
eye-gu:fix/13589-webclient-bodyinserter-npe

Conversation

@eye-gu

@eye-gu eye-gu commented Sep 22, 2026

Copy link
Copy Markdown
Member

Fix

  • Add a unit test to verify that the fix works.
  • Explain briefly why the bug exists and how to fix it.

@wu-sheng wu-sheng added the bug Something isn't working label Sep 22, 2026
@wu-sheng

Copy link
Copy Markdown
Member

The deferred injection in afterMethod breaks requests when the exchange Mono is re-subscribed, e.g. a retry inside an ExchangeFilterFunction:

.filter((req, next) -> next.exchange(req)
    .flatMap(r -> r.statusCode().is5xxServerError()
        ? r.releaseBody().then(Mono.<ClientResponse>error(new IllegalStateException("5xx")))
        : Mono.just(r))
    .retry(1))

With JdkClientHttpConnector (and Jetty / HttpComponents, which also call writeTo eagerly), writeTo runs only once at assembly, so the retry re-subscribes the same Mono.defer wrapper against the same ClientHttpRequest. The first attempt already committed that request, so getHeaders() returns read-only headers and set throws UnsupportedOperationException. The supplier runs inside the application's reactive chain, not inside InstMethodsInter, so the agent doesn't swallow the exception. It becomes the request's error.

I reproduced it against Spring 6.2.19 with the JDK connector, a server that returns 503 and then 200, and the deferred injection emulated as in this PR:

Case Result
no agent retry succeeds
deferred injection as in this PR request fails, cause UnsupportedOperationException
deferred injection wrapped in try/catch retry succeeds, a single sw8 value is sent

Before this PR the NPE was caught and only logged, so as written this turns a logging problem into a request failure. Could you guard the deferred injection in both the 5.x and 6.x interceptors? For example:

if (contextCarrier != null) {
    try {
        inject(clientHttpRequest, contextCarrier);
    } catch (Throwable t) {
        // headers are read-only once the request is committed (e.g. re-subscribed by a retry)
    }
}

When the exchange Mono is re-subscribed, e.g. by a retry in an
ExchangeFilterFunction, the deferred injection re-runs against the same
already committed ClientHttpRequest whose headers are read-only, and the
UnsupportedOperationException propagates into the application's reactive
chain and fails the request. Catch it in both the 5.x and 6.x
interceptors, as suggested in the review of apache#834: the sw8 header
injected on the first subscription is still sent on the retried request.
@eye-gu

eye-gu commented Sep 22, 2026

Copy link
Copy Markdown
Member Author

@wu-sheng Thanks! Fixed with the suggested try/catch in both interceptors.

@wu-sheng
wu-sheng merged commit 5cd38a5 into apache:main Sep 22, 2026
253 of 255 checks passed
@wu-sheng wu-sheng added this to the 9.8.0 milestone Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants