Skip to content

Support HTTP request trailers - #6278

Open
jnbdz wants to merge 5 commits into
eclipse-vertx:masterfrom
SiteNetSoft:request-trailers
Open

Support HTTP request trailers#6278
jnbdz wants to merge 5 commits into
eclipse-vertx:masterfrom
SiteNetSoft:request-trailers

Conversation

@jnbdz

@jnbdz jnbdz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #5253. Two commits: the server reading request trailers, and the client sending them.

HttpServerRequest has no way to read trailers, while HttpClientResponse exposes trailers() and getTrailer(). The trailers do arrive — Netty parses them — but the server drops them, and the code already marked both spots:

  • Http1ServerConnection.onContent: //TODO chunk trailers
  • Http2ServerConnectionImpl.onHeadersRead: // Http server request trailer - not implemented yet (in api)

HttpServerRequestImpl.handleTrailers(MultiMap) was already receiving the trailers and discarding them.

Change

HttpServerRequest gains getTrailer(String) and trailers(), declared exactly as on HttpClientResponse (@Nullable on the former, @CacheReturn on the latter). Before the request has ended, and for requests without trailers, trailers() returns an empty map rather than null — matching the client side.

  • HTTP/1.1Http1ServerConnection passes LastHttpContent.trailingHeaders() down, and Http1ServerRequest writes them into the inbound message queue rather than setting a field directly, so they cannot be observed before the preceding data has been delivered. This mirrors Http1ClientConnection.onEnd(LastHttpContent).
  • HTTP/2 (codec) — the trailing HEADERS frame is forwarded via stream.onTrailers(headers) instead of signalling empty trailers, mirroring Http2ClientConnectionImpl.
  • HTTP/2 (multiplex), HTTP/3, QUIC — already forwarded trailers correctly; unchanged.
  • HttpServerRequestWrapper gains the two delegating overrides.

Setting the trailers uses the same escape-safe merge as HttpClientResponseImpl.handleTrailers: if the caller already obtained the lazily-allocated empty map, it is updated in place instead of replaced.

Tests

Covering all three protocols:

  • HTTP/1.1Http1xTest.testRequestTrailers / testRequestNoTrailers, driven over a raw socket since the client cannot send trailers.
  • HTTP/2Http2ServerTest.testRequestTrailers / testRequestNoTrailers, sending a trailing HEADERS frame via the Netty test client. Because Http2MultiplexServerTest extends Http2ServerTest, these run against both the codec and multiplex connection implementations.
  • HTTP/3Http3ServerTest.testTrailers already sent request trailers but could only note // No API to get client trailers; it now asserts them.

Http1xTest (504) and Http2Test (359) pass. Each new assertion was checked by reverting the corresponding production change: dropping the HTTP/1 plumbing fails Http1xTest.testRequestTrailers, and dropping stream.onTrailers(...) in the HTTP/2 codec fails Http2ServerTest.testRequestTrailers.

Note: five testUpgradeToClearText* / testPriorKnowledge cases in Http2ServerTest fail on my machine with BindException: Address already in use on port 8080, identically on unmodified master — a local port conflict, unrelated to this change.

Second commit — client sending

HttpClientRequest gains trailers() and the four putTrailer overloads, declared exactly as on HttpServerResponse.

Trailers ride on the terminating chunk, so a request carrying them is always sent chunked — Netty silently discards trailing headers under Content-Length framing, which would yield a request that looks correct and arrives without its trailers. HttpClientRequestImpl drops Content-Length, forces Transfer-Encoding, writes the body with end=false and emits the trailers as the terminating write, mirroring HttpServerResponseImpl.

HttpClientStream gains writeHeaders(MultiMap, boolean) — the counterpart of the method HttpServerStream already had. HTTP/2 needs no new wire code (DefaultHttp2Stream already implements it, covering codec and multiplex); HTTP/3 reuses the server adapter; HTTP/1 is the only new wire path. HttpClientConnection gains newHttpTrailers() so the protocol-agnostic request can obtain a suitably backed map.

Because the client can now send trailers, the new tests live in HttpTest and therefore run under Http1xTest, Http2Test and Http3Test: trailers with a body, trailers without a body, and a request with none. Removing the chunked coercion fails testRequestTrailers rather than passing silently.

The raw-socket and raw-Netty tests from the first commit are kept on purpose — they show the server accepts trailers from a non-Vert.x client, which a test driving our own client cannot demonstrate.

Full run across the affected classes: 1200 tests, 0 failures. (Two BindException errors on my machine come from an unrelated process holding port 8080; they reproduce on unmodified master.)

Concurrency and async

Nothing here blocks — no await, executeBlocking, latch or synchronous result access is introduced on either side.

  • In HttpClientRequestImpl.doWrite, the trailers reference is captured inside the existing synchronized block, alongside the other values used after it (chunked, writeEnd, writeHead), so it is safely published to whichever thread performs the write.
  • The trailers write is composed onto the body write rather than issued independently: the future returned by end() completes only once the trailers are on the wire, and a failed body write short-circuits so trailers are never emitted onto a broken stream.
  • The new HTTP/1 writeTrailers goes through the connection's outbound MessageWrite queue like an ordinary body write, so back-pressure behaviour is unchanged, and every path — stream reset, cancel, successful write — completes the promise. (A missed path there would hang a request rather than fail it.)
  • Http2UpgradeClientConnection.UpgradingStream.writeHeaders re-dispatches to the event loop when invoked from another thread and still fires SEND_BUFFERED_MESSAGES_EVENT on end, mirroring writeChunk.
  • On the server side, trailers travel through the request's inbound message queue rather than being assigned to a field directly, so they cannot be observed ahead of the data that preceded them under pause/resume.

The synchronized (conn) usage follows the existing idiom in these classes: short critical sections with no I/O performed under the lock.

I also have this backported to 4.x if a maintenance backport is wanted.

@vietj vietj left a comment

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.

we need a test for HTTP/2 and HTTP/3 as well

HttpServerRequest had no way to access trailers, while HttpClientResponse
exposes trailers() and getTrailer(). Trailers were parsed by Netty and then
dropped: Http1ServerConnection carried a "//TODO chunk trailers" comment, and
the HTTP/2 codec connection noted "not implemented yet (in api)".

The public interface gains getTrailer(String) and trailers(), mirroring
HttpClientResponse, and returning an empty map before the request has ended
rather than null.

For HTTP/1 the trailers travel through the request's inbound message queue,
as the client already does, so they cannot be observed before the preceding
data has been delivered. For HTTP/2 the codec connection now forwards the
trailing HEADERS frame to the stream instead of signalling empty trailers;
the multiplex, HTTP/3 and QUIC paths already forwarded them, and
HttpServerRequestImpl already received the MultiMap and discarded it.

Fixes eclipse-vertx#5253

Signed-off-by: jnbdz <jn@yaloub.com>
@jnbdz
jnbdz force-pushed the request-trailers branch from edf5ac2 to 05f1416 Compare August 4, 2026 16:02
@jnbdz jnbdz changed the title Support reading HTTP request trailers on the server Aug 4, 2026
Completes eclipse-vertx#5253. The server side of the same commit lets a Vert.x server read
request trailers; this lets a Vert.x client send them, so the feature no
longer has to be tested by hand-writing chunked bytes onto a socket.

HttpClientRequest gains trailers() and the four putTrailer overloads,
declared exactly as on HttpServerResponse.

Trailers ride on the terminating chunk, so a request carrying them is always
sent chunked: Netty silently discards trailing headers under Content-Length
framing, which would produce a request that looks fine and arrives without
its trailers. HttpClientRequestImpl therefore drops Content-Length and forces
Transfer-Encoding when trailers are set, writes the body with end=false, and
emits the trailers as the terminating write — mirroring how
HttpServerResponseImpl already writes response trailers.

HttpClientStream gains writeHeaders(MultiMap, boolean), the counterpart of the
method HttpServerStream already had. HTTP/2 needs no wire code because
DefaultHttp2Stream already implements it; HTTP/3 reuses the server adapter;
HTTP/1 is the only new wire path. HttpClientConnection gains newHttpTrailers()
so the protocol-agnostic request can obtain a suitably backed map.

Tests go in HttpTest, which Http1xTest, Http2Test and Http3Test all extend, so
they run across all three protocols: trailers with a body, trailers without a
body, and a request with none. Removing the chunked coercion fails
testRequestTrailers rather than passing silently.

The existing raw-socket and raw-Netty tests are kept deliberately: they prove
the server accepts trailers from a non-Vert.x client, which a test driving our
own client cannot show.
@jnbdz
jnbdz force-pushed the request-trailers branch from 0392d4a to db3786b Compare August 4, 2026 17:22
@jnbdz

jnbdz commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — those are in now; the review landed just before I pushed them.

HTTP/2Http2ServerTest.testRequestTrailers / testRequestNoTrailers, sending a trailing HEADERS frame. Since Http2MultiplexServerTest extends Http2ServerTest these run against both the codec and multiplex connection implementations.

HTTP/3Http3ServerTest.testTrailers already sent request trailers and carried a // No API to get client trailers comment; it now asserts them and the comment is gone.

A second commit adds the client side (HttpClientRequest.putTrailer), which let me put cross-protocol tests in HttpTest — so testRequestTrailers, testRequestTrailersWithoutBody and testRequestNoTrailers also run under Http1xTest, Http2Test and Http3Test.

The raw-socket and raw-Netty tests are kept deliberately: they show the server accepts trailers from a non-Vert.x client.

Happy to split the client side into its own PR if you would rather review them separately.

Comment thread vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientStream.java Outdated
Comment thread vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerRequestImpl.java Outdated
Comment thread vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java Outdated
Comment thread vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java Outdated
Comment thread vertx-core/src/test/java/io/vertx/tests/http/Http2ServerTest.java

@vietj vietj left a comment

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.

Comments

jnbdz and others added 2 commits August 13, 2026 09:40
Address review comments: pull the shared writeHeaders(MultiMap, boolean)
declaration up from HttpClientStream/HttpServerStream to HttpStream, drop
redundant comments, and mirror the HTTP/2 request trailer tests in
Http3ServerTest.
@jnbdz

jnbdz commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@vietj a scope question for a possible follow-up, not for this PR:

With request trailers in, Vert.x carries the trailer fields in every direction, but neither side ever touches the Trailer header (RFC 9110 §6.6.2) that declares which fields will arrive as trailers — putTrailer doesn't add it, and it isn't read on receipt. Today that declaration is left to the application, which on HTTP/1.1 has to set it before the headers are committed, i.e. know the trailer names up front.

Would you want Vert.x to emit Trailer: <names> itself when putTrailer has been called on an HTTP/1.1 response (or request) before the headers are written, and leave an explicitly set Trailer header alone? It is a SHOULD in the RFC and Netty doesn't do it either, so it's equally defensible to keep it application-level — I'd rather know which way you lean before writing anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants