Skip to content

[fix][broker][branch-4.2] Fix admin API HTTP 400 FAIL_ON_TRAILING_TOKENS when a broker interceptor is loaded - #26223

Merged
lhotari merged 1 commit into
apache:branch-4.2from
lhotari:lh-fix-26185-request-wrapper-4.2
Jul 22, 2026
Merged

lhotari merged 1 commit into
apache:branch-4.2from
lhotari:lh-fix-26185-request-wrapper-4.2

Conversation

@lhotari

@lhotari lhotari commented Jul 22, 2026 •

Copy link
Copy Markdown
Member

Fixes #26185

Motivation

When any broker interceptor NAR is loaded, WebService registers PreInterceptFilter, which wraps every incoming HTTP request in RequestWrapper so the body can be read multiple times. The branch-4.2 RequestWrapper:

  • eagerly reads the request body in the constructor through an InputStreamReader/charset round-trip, and
  • returns a new ServletInputStream for every getInputStream() call, with isFinished() hardcoded to false.

Jersey's Jackson provider enables DeserializationFeature.FAIL_ON_TRAILING_TOKENS unconditionally (via JaxRSFeature.READ_FULL_STREAM, default since jackson-jaxrs-providers 2.15). When a reader re-fetches the input stream after reaching EOF, the fresh stream replays the body from the first byte again, which surfaces as:

HTTP 400 Trailing token (of type START_OBJECT) found after value ...: not allowed as per `DeserializationFeature.FAIL_ON_TRAILING_TOKENS`

on admin POST/PUT requests with a JSON body (reset cursor, set retention, bookie racks-info, etc.) whenever a broker interceptor is loaded. See the detailed root-cause analysis in #26185; the reporters confirmed that applying the master version of RequestWrapper to a 4.0.12 broker resolves the failures while the interceptor stays loaded.

Independently of the replay bug, the constructor's charset round-trip is a latent body-corruption risk (it is not what triggered the 400s in #26185 — the reporters verified the round-trip is byte-for-byte lossless for their plain-ASCII JSON): it decodes the raw body with the JVM's default charset and re-encodes it with that same charset, ignoring the charset declared by the request. Concretely:

  • When the JVM default charset is not UTF-8 — possible on JDK 17 (this branch's broker target), where the default derives from the OS locale (e.g. US-ASCII under a POSIX/C locale), and on JDK 18+ via -Dfile.encoding overrides — a UTF-8 JSON body containing non-ASCII characters (unicode values in policies, subscription properties, etc.) can be silently corrupted: undecodable bytes are replaced with U+FFFD/? before the body reaches Jersey.
  • Even with a UTF-8 default charset, a body that is not valid UTF-8 (e.g. a UTF-16-encoded JSON body, which Jackson auto-detects and accepts, or any malformed sequence) is mutated by replacement characters, and the re-encoded buffer's length can then disagree with the request's declared Content-Length, which the wrapper does not override.

The master implementation eliminates this entire class of problems by buffering the raw bytes with no decode/re-encode step.

master already contains the fixed RequestWrapper — the change shipped as part of the PIP-472 jakarta migration (#25912) — so no master-side change is needed; this PR brings the same code to branch-4.2.

Modifications

Copy RequestWrapper from master verbatim, with jakarta.servlet imports mapped back to javax.servlet (the only adaptation):

  • Buffer the request body lazily on first access, bounded by Content-Length, reading raw bytes without a charset round-trip — this also removes the charset-dependent body-corruption risk described in the motivation. An interceptor that never reads the body (the common case) no longer causes the body to be buffered at all.
  • Cache and return a single stable ServletInputStream across getInputStream() calls (per the Servlet contract), with a working isFinished(), so an EOF'd stream can no longer be replayed from the start.
  • getBody() now declares throws IOException, matching master. This is binary-compatible for existing compiled interceptor NARs.

Verifying this change

  • Make sure that the change passes the CI checks.

This change is already covered by existing tests, such as BrokerInterceptorTest (admin API calls with JSON bodies while interceptors are loaded, e.g. testWebserviceRequest) and InterceptFilterOutTest. The fix itself was validated by the issue reporters against a live 4.0.12 broker with an interceptor NAR loaded (see #26185).

Does this pull request potentially affect one of the following parts:

If the box was checked, please highlight the changes

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

…ENS when a broker interceptor is loaded

Copy RequestWrapper from master, where this fix shipped as part of apache#25912:
- buffer the request body lazily on first access, bounded by
  Content-Length, reading raw bytes without a charset round-trip
- cache a single stable ServletInputStream across getInputStream() calls
  (Servlet contract) so a reader that re-fetches the stream after EOF
  cannot re-read the body from the first byte, which surfaced as Jackson
  FAIL_ON_TRAILING_TOKENS (HTTP 400) on admin POST/PUT requests whenever
  a broker interceptor NAR is loaded

Fixes apache#26185
@lhotari
lhotari merged commit 7a172ad into apache:branch-4.2 Jul 22, 2026
50 of 54 checks passed
lhotari added a commit that referenced this pull request Jul 22, 2026
…ENS when a broker interceptor is loaded (#26223)

(cherry picked from commit 7a172ad)
sandeep-ctds pushed a commit to datastax/pulsar that referenced this pull request Jul 31, 2026
…ENS when a broker interceptor is loaded (apache#26223)

(cherry picked from commit 7a172ad)
nodece pushed a commit to ascentstream/pulsar that referenced this pull request Aug 28, 2026
…ENS when a broker interceptor is loaded (apache#26223)

(cherry picked from commit 7a172ad)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant