Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/main/java/io/vertx/httpproxy/impl/HttpUtils.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
}
}
25 changes: 17 additions & 8 deletions src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -266,15 +265,25 @@ public ProxyResponse release() {

private Future<Void> sendResponse(ReadStream<Buffer> body) {
Pipe<Buffer> 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();
}
});
}
}
61 changes: 60 additions & 1 deletion src/test/java/io/vertx/tests/ProxyTest.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -231,4 +231,63 @@ public Future<ProxyResponse> 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<Void> 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"));
}
}));
}
}
Loading