Skip to content

Commit 1f22d0f

Browse files
committed
Support reading HTTP request trailers on the server
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 #5253
1 parent abab114 commit 1f22d0f

7 files changed

Lines changed: 161 additions & 8 deletions

File tree

vertx-core/src/main/java/io/vertx/core/http/HttpServerRequest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,29 @@ default boolean isSSL() {
149149
@CacheReturn
150150
HttpServerResponse response();
151151

152+
/**
153+
* Return the first trailer value with the specified name
154+
* <p>
155+
* Trailers are only available after the request has been fully received, i.e. after the
156+
* {@link #endHandler(Handler) end handler} has been called.
157+
*
158+
* @param trailerName the trailer name
159+
* @return the trailer value
160+
*/
161+
@Nullable String getTrailer(String trailerName);
162+
163+
/**
164+
* Return the trailers.
165+
* <p>
166+
* Trailers are only available after the request has been fully received, i.e. after the
167+
* {@link #endHandler(Handler) end handler} has been called. Before that, and for requests
168+
* that carry no trailers, the returned map is empty.
169+
*
170+
* @return the trailers
171+
*/
172+
@CacheReturn
173+
MultiMap trailers();
174+
152175
/**
153176
* Override the charset to use for decoding the query parameter map, when none is set, {@code UTF8} is used.
154177
*

vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerRequestImpl.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.vertx.core.http.HttpMethod;
2828
import io.vertx.core.http.HttpVersion;
2929
import io.vertx.core.http.*;
30+
import io.vertx.core.http.impl.headers.HeadersAdaptor;
3031
import io.vertx.core.internal.ContextInternal;
3132
import io.vertx.core.internal.http.QueryParamDecoder;
3233
import io.vertx.core.net.HostAndPort;
@@ -60,6 +61,7 @@ public class HttpServerRequestImpl extends HttpServerRequestBase {
6061
private HostAndPort realAuthority;
6162
private String absoluteURI;
6263
private MultiMap attributes;
64+
private MultiMap trailers;
6365
private HttpEventHandler eventHandler;
6466
private boolean ended;
6567
private Handler<HttpServerFileUpload> uploadHandler;
@@ -188,6 +190,13 @@ public void handleData(Buffer data) {
188190
public void handleTrailers(MultiMap trailers) {
189191
HttpEventHandler handler;
190192
synchronized (connection) {
193+
// Setting the trailers must not race with trailers(), where the field can escape:
194+
// if the user already obtained the empty map, update it in place instead.
195+
if (this.trailers == null) {
196+
this.trailers = trailers;
197+
} else if (this.trailers != trailers) {
198+
this.trailers.setAll(trailers);
199+
}
191200
ended = true;
192201
if (postRequestDecoder != null) {
193202
try {
@@ -374,6 +383,25 @@ public MultiMap headers() {
374383
return headersMap;
375384
}
376385

386+
@Override
387+
public MultiMap trailers() {
388+
synchronized (connection) {
389+
if (trailers == null) {
390+
trailers = new HeadersAdaptor(new DefaultHttpHeaders());
391+
}
392+
return trailers;
393+
}
394+
}
395+
396+
@Override
397+
public String getTrailer(String trailerName) {
398+
MultiMap trailers;
399+
synchronized (connection) {
400+
trailers = this.trailers;
401+
}
402+
return trailers != null ? trailers.get(trailerName) : null;
403+
}
404+
377405
@Override
378406
public SocketAddress remoteAddress() {
379407
return super.remoteAddress();

vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerConnection.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,18 +246,21 @@ private void onContent(Object msg) {
246246
Buffer buffer = BufferInternal.safeBuffer(content.content());
247247
Http1ServerRequest request = requestInProgress;
248248
request.handleContent(buffer);
249-
//TODO chunk trailers
250249
if (content instanceof LastHttpContent) {
251-
onEnd();
250+
onEnd(((LastHttpContent) content).trailingHeaders());
252251
}
253252
}
254253

255254
private void onEnd() {
255+
onEnd(null);
256+
}
257+
258+
private void onEnd(HttpHeaders trailers) {
256259
boolean tryClose;
257260
Http1ServerRequest request = requestInProgress;
258261
requestInProgress = null;
259262
tryClose = (wantClose || shutdownInitiated != null) && responseInProgress == null;
260-
request.handleEnd();
263+
request.handleEnd(trailers);
261264
if (tryClose) {
262265
closeInternal();
263266
}

vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public class Http1ServerRequest extends HttpServerRequestBase implements io.vert
7171

7272
// Cache this for performance
7373
private MultiMap headers;
74+
private MultiMap trailers;
7475
private String absoluteURI;
7576

7677
private HttpEventHandler eventHandler;
@@ -104,6 +105,11 @@ private InboundMessageQueue<Object> queue(boolean create) {
104105
protected void handleMessage(Object elt) {
105106
if (elt == InboundBuffer.END_SENTINEL) {
106107
onEnd();
108+
} else if (elt instanceof MultiMap) {
109+
// Trailers travel through the queue so they cannot be observed before the
110+
// preceding data has been delivered.
111+
setTrailers((MultiMap) elt);
112+
onEnd();
107113
} else {
108114
onData((Buffer) elt);
109115
}
@@ -155,22 +161,40 @@ void handleContent(Buffer buffer) {
155161
}
156162
}
157163

158-
void handleEnd() {
164+
void handleEnd(HttpHeaders nettyTrailers) {
165+
Object end = nettyTrailers != null && !nettyTrailers.isEmpty()
166+
? new HeadersAdaptor(nettyTrailers)
167+
: InboundBuffer.END_SENTINEL;
159168
InboundMessageQueue<Object> queue = queue(false);
160169
if (queue != null) {
161-
handleEnd(queue);
170+
handleEnd(queue, end);
162171
} else {
172+
if (end != InboundBuffer.END_SENTINEL) {
173+
setTrailers((MultiMap) end);
174+
}
163175
context.execute(this, Http1ServerRequest::onEnd);
164176
}
165177
}
166178

167-
private void handleEnd(InboundMessageQueue<Object> queue) {
168-
boolean drain = queue.add(InboundBuffer.END_SENTINEL);
179+
private void handleEnd(InboundMessageQueue<Object> queue, Object end) {
180+
boolean drain = queue.add(end);
169181
if (drain) {
170182
queue.drain();
171183
}
172184
}
173185

186+
private void setTrailers(MultiMap trailers) {
187+
synchronized (conn) {
188+
// Must not race with trailers(), where the field can escape: if the user already
189+
// obtained the empty map, update it in place instead of replacing it.
190+
if (this.trailers == null) {
191+
this.trailers = trailers;
192+
} else if (this.trailers != trailers) {
193+
this.trailers.setAll(trailers);
194+
}
195+
}
196+
}
197+
174198
private void check100() {
175199
if (HttpUtil.is100ContinueExpected(request)) {
176200
response.writeContinue();
@@ -282,6 +306,25 @@ public HostAndPort authority(boolean real) {
282306
return real ? null : authority();
283307
}
284308

309+
@Override
310+
public MultiMap trailers() {
311+
synchronized (conn) {
312+
if (trailers == null) {
313+
trailers = new HeadersAdaptor(new DefaultHttpHeaders());
314+
}
315+
return trailers;
316+
}
317+
}
318+
319+
@Override
320+
public String getTrailer(String trailerName) {
321+
MultiMap trailers;
322+
synchronized (conn) {
323+
trailers = this.trailers;
324+
}
325+
return trailers != null ? trailers.get(trailerName) : null;
326+
}
327+
285328
@Override
286329
public long bytesRead() {
287330
synchronized (conn) {

vertx-core/src/main/java/io/vertx/core/http/impl/http2/codec/Http2ServerConnectionImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,10 @@ protected synchronized void onHeadersRead(int streamId, Http2Headers headers, St
148148
stream.context().execute(stream.unwrap(), streamHandler);
149149
stream.onHeaders(headersMap);
150150
} else {
151-
// Http server request trailer - not implemented yet (in api)
151+
// Trailing HEADERS frame: carry the trailers to the request instead of discarding them.
152152
stream = nettyStream.getProperty(streamKey);
153+
stream.onTrailers(new HttpHeaders(headers));
154+
return;
153155
}
154156
if (endOfStream) {
155157
stream.onTrailers();

vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestWrapper.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ public MultiMap headers() {
130130
return delegate.headers();
131131
}
132132

133+
@Override
134+
public String getTrailer(String trailerName) {
135+
return delegate.getTrailer(trailerName);
136+
}
137+
138+
@Override
139+
public MultiMap trailers() {
140+
return delegate.trailers();
141+
}
142+
133143
@Override
134144
public HttpServerRequest setParamsCharset(String charset) {
135145
return delegate.setParamsCharset(charset);

vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3395,6 +3395,50 @@ public void testTooLongContentInHttpServerRequest(Checkpoint checkpoint) throws
33953395
.write("POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\ntoolong\r\n")
33963396
.await();
33973397
}
3398+
@Test
3399+
public void testRequestTrailers(Checkpoint checkpoint) throws Exception {
3400+
server.requestHandler(req -> {
3401+
req.endHandler(v -> {
3402+
assertEquals("chunky", req.getTrailer("X-Trailer"));
3403+
assertEquals("other", req.getTrailer("X-Other"));
3404+
assertEquals(2, req.trailers().size());
3405+
checkpoint.succeed();
3406+
});
3407+
req.response().end();
3408+
});
3409+
startServer(testAddress);
3410+
NetClient client = vertx.createNetClient();
3411+
NetSocket so = client.connect(testAddress).await();
3412+
so.write("POST / HTTP/1.1\r\n" +
3413+
"Host: localhost\r\n" +
3414+
"Transfer-Encoding: chunked\r\n" +
3415+
"Trailer: X-Trailer\r\n" +
3416+
"\r\n" +
3417+
"5\r\nhello\r\n" +
3418+
"0\r\n" +
3419+
"X-Trailer: chunky\r\n" +
3420+
"X-Other: other\r\n" +
3421+
"\r\n");
3422+
checkpoint.awaitSuccess();
3423+
}
3424+
3425+
@Test
3426+
public void testRequestNoTrailers(Checkpoint checkpoint) throws Exception {
3427+
server.requestHandler(req -> {
3428+
req.endHandler(v -> {
3429+
assertTrue(req.trailers().isEmpty());
3430+
assertNull(req.getTrailer("X-Trailer"));
3431+
checkpoint.succeed();
3432+
});
3433+
req.response().end();
3434+
});
3435+
startServer(testAddress);
3436+
client.request(requestOptions)
3437+
.compose(req -> req.send(Buffer.buffer("hello")))
3438+
.await();
3439+
checkpoint.awaitSuccess();
3440+
}
3441+
33983442
@Test
33993443
public void testInvalidTrailerInHttpServerRequest(Checkpoint checkpoint) throws Exception {
34003444
testHttpServerRequestDecodeError(checkpoint, so -> {

0 commit comments

Comments
 (0)