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 pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
<version>3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -133,4 +139,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
9 changes: 5 additions & 4 deletions src/main/java/io/vertx/httpproxy/impl/HttpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpVersion;

import java.time.Instant;
import java.util.List;
Expand Down Expand Up @@ -54,8 +55,8 @@ static Instant dateHeader(MultiMap headers) {
}
}

public static boolean trailersSupported(HttpServerResponse proxiedResponse) {
return proxiedResponse.streamId() >= 0 // HTTP/2 and HTTP/3
|| proxiedResponse.isChunked(); // Required for HTTP/1.1
static boolean isNotHttp1x(HttpServerRequest request) {
HttpVersion httpVersion = request.connection().protocolVersion();
return httpVersion != HttpVersion.HTTP_1_0 && httpVersion != HttpVersion.HTTP_1_1;
}
}
5 changes: 3 additions & 2 deletions src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@ Future<ProxyResponse> sendRequest() {
if (len >= 0) {
request.putHeader(CONTENT_LENGTH, Long.toString(len));
} else {
Boolean isChunked = HttpUtils.isChunked(proxiedRequest.headers());
request.setChunked(len == -1 && Boolean.TRUE == isChunked);
boolean isChunked = HttpUtils.isNotHttp1x(proxiedRequest)
|| Boolean.TRUE == HttpUtils.isChunked(proxiedRequest.headers());
request.setChunked(isChunked);
}

Pipe<Buffer> pipe = body.stream().pipe();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private Future<Void> sendResponse(ReadStream<Buffer> body) {
// Only forward trailers if using the original backend response stream
if (body.equals(response)) {
MultiMap trailers = response.trailers();
if (!trailers.isEmpty() && HttpUtils.trailersSupported(proxiedResponse)) {
if (!trailers.isEmpty() && (HttpUtils.isNotHttp1x(request.proxiedRequest()) || proxiedResponse.isChunked())) {
proxiedResponse.trailers().addAll(trailers);
}
}
Expand Down
156 changes: 156 additions & 0 deletions src/test/java/io/vertx/tests/grpc/GrpcProxyIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/
package io.vertx.tests.grpc;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.WaitContainerCmd;
import com.github.dockerjava.api.command.WaitContainerResultCallback;
import io.netty.util.internal.PlatformDependent;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.SocketAddress;
import io.vertx.httpproxy.HttpProxy;
import io.vertx.httpproxy.ProxyOptions;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.InternetProtocol;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.builder.ImageFromDockerfile;

import java.time.Duration;

import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeFalse;

public class GrpcProxyIntegrationTest {

private static final int GRPC_SERVER_PORT = 50051;
private static final int PROXY_PORT = 8080;

private Vertx vertx;
private HttpServer proxyServer;
private HttpClient httpClient;
private ServerContainer<?> grpcServerContainer;
private GenericContainer<?> grpcClientContainer;

@BeforeClass
public static void beforeClass() throws Exception {
assumeFalse("Cannot run Linux containers on Windows", PlatformDependent.isWindows());
}

@Before
public void setUp() throws Exception {
vertx = Vertx.vertx();
}

@After
public void tearDownContainers() {
if (grpcClientContainer != null) {
grpcClientContainer.stop();
}
if (grpcServerContainer != null) {
grpcServerContainer.stop();
}
if (proxyServer != null) {
proxyServer.close().await();
}
if (httpClient != null) {
httpClient.close().await();
}
if (vertx != null) {
vertx.close().await();
}
}

@Test
public void testGrpcThroughProxy() throws Exception {
startGrpcServer();
startProxy();
int exitCode = runGrpcClient();
assertEquals("gRPC client tests should pass", 0, exitCode);
}

private void startProxy() {
httpClient = vertx.createHttpClient(new HttpClientOptions()
.setProtocolVersion(io.vertx.core.http.HttpVersion.HTTP_2)
.setHttp2ClearTextUpgrade(false));

SocketAddress backend = SocketAddress.inetSocketAddress(grpcServerContainer.getMappedPort(GRPC_SERVER_PORT), grpcServerContainer.getHost());
HttpProxy proxy = HttpProxy.reverseProxy(new ProxyOptions(), httpClient).origin(backend);

proxyServer = vertx.createHttpServer(new HttpServerOptions()
.setPort(PROXY_PORT)
.setHost("0.0.0.0")
.setHttp2ClearTextEnabled(true))
.requestHandler(proxy)
.listen()
.await();
}

private void startGrpcServer() throws Exception {
grpcServerContainer = new ServerContainer<>(new ImageFromDockerfile("vertx-http-proxy-grpc-server", false)
.withFileFromClasspath("Dockerfile", "grpc/server/Dockerfile")
.withFileFromClasspath("server.js", "grpc/server/server.js")
.withFileFromClasspath("package.json", "grpc/package.json")
.withFileFromClasspath("test.proto", "grpc/test.proto"));
if (System.getProperties().containsKey("containerFixedPort")) {
grpcServerContainer.withFixedExposedPort(GRPC_SERVER_PORT, GRPC_SERVER_PORT);
} else {
grpcServerContainer.withExposedPorts(GRPC_SERVER_PORT);
}
grpcServerContainer
.withEnv("GRPC_PORT", String.valueOf(GRPC_SERVER_PORT))
.withEnv("GRPC_HOST", "0.0.0.0")
.waitingFor(Wait.forLogMessage(".*gRPC server listening.*", 1));

grpcServerContainer.start();
}

private int runGrpcClient() throws Exception {
grpcClientContainer = new GenericContainer<>(
new ImageFromDockerfile("vertx-http-proxy-grpc-client", false)
.withFileFromClasspath("Dockerfile", "grpc/client/Dockerfile")
.withFileFromClasspath("client.js", "grpc/client/client.js")
.withFileFromClasspath("package.json", "grpc/package.json")
.withFileFromClasspath("test.proto", "grpc/test.proto"))
.withNetworkMode("host")
.withEnv("GRPC_SERVER", String.format("localhost:%d", PROXY_PORT))
.withStartupCheckStrategy(new OneShotStartupCheckStrategy())
.withStartupTimeout(Duration.ofMinutes(3));

grpcClientContainer.start();

DockerClient dockerClient = grpcClientContainer.getDockerClient();
try (WaitContainerCmd cmd = dockerClient.waitContainerCmd(grpcClientContainer.getContainerId())) {
return cmd.exec(new WaitContainerResultCallback())
.awaitStatusCode();
}
}

private static class ServerContainer<SELF extends ServerContainer<SELF>> extends GenericContainer<SELF> {

public ServerContainer(java.util.concurrent.Future<String> dockerImageName) {
super(dockerImageName);
}

public SELF withFixedExposedPort(int hostPort, int containerPort) {
super.addFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP);
return self();
}
}
}
4 changes: 4 additions & 0 deletions src/test/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
requires io.vertx.testing.unit;
requires junit;
requires wiremock.standalone;
requires testcontainers;
requires com.github.dockerjava.api;
requires org.apache.commons.lang3;
requires io.netty.common;
}
16 changes: 16 additions & 0 deletions src/test/resources/grpc/client/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM fedora:43 AS base

RUN dnf install -y nodejs npm && dnf clean all

WORKDIR /app

COPY package.json ./
RUN npm install

COPY test.proto ./

FROM base AS client

COPY client.js ./

CMD ["node", "client.js"]
Loading
Loading