diff --git a/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java b/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java index 4aa83f4..c392206 100644 --- a/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java +++ b/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -12,6 +12,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; import java.time.Instant; import java.util.List; @@ -52,4 +53,9 @@ static Instant dateHeader(MultiMap headers) { return ParseUtils.parseHeaderDate(dateHeader); } } + + public static boolean trailersSupported(HttpServerResponse proxiedResponse) { + return proxiedResponse.streamId() >= 0 // HTTP/2 and HTTP/3 + || proxiedResponse.isChunked(); // Required for HTTP/1.1 + } } diff --git a/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java b/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java index 2d255ee..5c98d3b 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java +++ b/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -21,7 +21,6 @@ import io.vertx.core.streams.Pipe; import io.vertx.core.streams.ReadStream; import io.vertx.httpproxy.Body; -import io.vertx.httpproxy.MediaType; import io.vertx.httpproxy.ProxyRequest; import io.vertx.httpproxy.ProxyResponse; @@ -266,15 +265,25 @@ public ProxyResponse release() { private Future sendResponse(ReadStream body) { Pipe pipe = body.pipe(); - pipe.endOnSuccess(true); + pipe.endOnSuccess(false); pipe.endOnFailure(false); return pipe .to(proxiedResponse) + .compose(v -> { + // Only forward trailers if using the original backend response stream + if (body.equals(response)) { + MultiMap trailers = response.trailers(); + if (!trailers.isEmpty() && HttpUtils.trailersSupported(proxiedResponse)) { + proxiedResponse.trailers().addAll(trailers); + } + } + return proxiedResponse.end(); + }) .andThen(ar -> { - if (ar.failed()) { - request.request.reset(); - proxiedResponse.reset(); - } - }); + if (ar.failed()) { + request.request.reset(); + proxiedResponse.reset(); + } + }); } } diff --git a/src/test/java/io/vertx/tests/ProxyTest.java b/src/test/java/io/vertx/tests/ProxyTest.java index d55bd3a..113591d 100644 --- a/src/test/java/io/vertx/tests/ProxyTest.java +++ b/src/test/java/io/vertx/tests/ProxyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -231,4 +231,63 @@ public Future handleProxyRequest(ProxyContext context) { ) .onComplete(ctx.asyncAssertSuccess(buffer -> ctx.assertEquals("HOLA", buffer.toString()))); } + + @Test + public void testTrailersForwarding(TestContext ctx) { + testTrailers(ctx, null); + } + + @Test + public void testTrailersNotForwardedWhenBodyIsModified(TestContext ctx) { + testTrailers(ctx, new ProxyInterceptor() { + @Override + public Future handleProxyResponse(ProxyContext context) { + ProxyResponse proxyResponse = context.response(); + proxyResponse.setBody(Body.body(io.vertx.core.buffer.Buffer.buffer("modified body"))); + return context.sendResponse(); + } + }); + } + + @Test + public void testTrailersNotForwardedWhenBodyIsTransformed(TestContext ctx) { + ProxyInterceptor interceptor = ProxyInterceptor.builder() + .transformingResponseBody(BodyTransformers.text(String::toUpperCase, "UTF-8")) + .build(); + testTrailers(ctx, interceptor); + } + + private void testTrailers(TestContext ctx, ProxyInterceptor interceptor) { + SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + req.response() + .setChunked(true) + .putHeader("content-type", "text/plain") + .write("response body"); + req.response().trailers() + .add("X-Custom-Trailer", "trailer-value") + .add("X-Another-Trailer", "another-value"); + req.response().end(); + }); + startProxy(proxy -> { + proxy.origin(backend); + if (interceptor != null) { + proxy.addInterceptor(interceptor); + } + }); + client = vertx.createHttpClient(); + client.request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req.send().compose(resp -> { + ctx.assertEquals(200, resp.statusCode()); + return resp.body().map(body -> resp); + })) + .onComplete(ctx.asyncAssertSuccess(resp -> { + if (interceptor == null) { + ctx.assertEquals("trailer-value", resp.trailers().get("X-Custom-Trailer")); + ctx.assertEquals("another-value", resp.trailers().get("X-Another-Trailer")); + } else { + ctx.assertNull(resp.trailers().get("X-Custom-Trailer")); + ctx.assertNull(resp.trailers().get("X-Another-Trailer")); + } + })); + } }