Skip to content

Commit 5ef0668

Browse files
authored
Support gRPC (#147)
* Support gRPC Closes #132 Added a test that starts a proxy in between gRPC client/server. To avoid polluting dependencies, both the client and the server run in containers. Request/Reply, Streaming and Bidirectional Streaming modes are tested. Improved ProxiedRequest because for HTTP/2 and HTTP/3, the content-length header is optional. Without this change, the Vert.x HTTP Client throws a runtime exception because the request was not flagged with `chunked`. Some portions of this content were created with the assistance of IBM Bob. Signed-off-by: Thomas Segismont <tsegismont@gmail.com> * Skip the GrpcProxyIntegrationTest on Windows Also, make the tearDown method more robust Signed-off-by: Thomas Segismont <tsegismont@gmail.com> * Don't rely on streamId to determine if the protocol version is not HTTP/1.x Signed-off-by: Thomas Segismont <tsegismont@gmail.com> --------- Signed-off-by: Thomas Segismont <tsegismont@gmail.com>
1 parent 860ce93 commit 5ef0668

12 files changed

Lines changed: 510 additions & 8 deletions

File tree

pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@
8989
<version>3.0.1</version>
9090
<scope>test</scope>
9191
</dependency>
92+
<dependency>
93+
<groupId>org.testcontainers</groupId>
94+
<artifactId>testcontainers</artifactId>
95+
<version>2.0.4</version>
96+
<scope>test</scope>
97+
</dependency>
9298
</dependencies>
9399

94100
<build>
@@ -133,4 +139,4 @@
133139
</plugin>
134140
</plugins>
135141
</build>
136-
</project>
142+
</project>

src/main/java/io/vertx/httpproxy/impl/HttpUtils.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
import io.vertx.core.MultiMap;
1414
import io.vertx.core.http.HttpHeaders;
15-
import io.vertx.core.http.HttpServerResponse;
15+
import io.vertx.core.http.HttpServerRequest;
16+
import io.vertx.core.http.HttpVersion;
1617

1718
import java.time.Instant;
1819
import java.util.List;
@@ -54,8 +55,8 @@ static Instant dateHeader(MultiMap headers) {
5455
}
5556
}
5657

57-
public static boolean trailersSupported(HttpServerResponse proxiedResponse) {
58-
return proxiedResponse.streamId() >= 0 // HTTP/2 and HTTP/3
59-
|| proxiedResponse.isChunked(); // Required for HTTP/1.1
58+
static boolean isNotHttp1x(HttpServerRequest request) {
59+
HttpVersion httpVersion = request.connection().protocolVersion();
60+
return httpVersion != HttpVersion.HTTP_1_0 && httpVersion != HttpVersion.HTTP_1_1;
6061
}
6162
}

src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,9 @@ Future<ProxyResponse> sendRequest() {
233233
if (len >= 0) {
234234
request.putHeader(CONTENT_LENGTH, Long.toString(len));
235235
} else {
236-
Boolean isChunked = HttpUtils.isChunked(proxiedRequest.headers());
237-
request.setChunked(len == -1 && Boolean.TRUE == isChunked);
236+
boolean isChunked = HttpUtils.isNotHttp1x(proxiedRequest)
237+
|| Boolean.TRUE == HttpUtils.isChunked(proxiedRequest.headers());
238+
request.setChunked(isChunked);
238239
}
239240

240241
Pipe<Buffer> pipe = body.stream().pipe();

src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ private Future<Void> sendResponse(ReadStream<Buffer> body) {
273273
// Only forward trailers if using the original backend response stream
274274
if (body.equals(response)) {
275275
MultiMap trailers = response.trailers();
276-
if (!trailers.isEmpty() && HttpUtils.trailersSupported(proxiedResponse)) {
276+
if (!trailers.isEmpty() && (HttpUtils.isNotHttp1x(request.proxiedRequest()) || proxiedResponse.isChunked())) {
277277
proxiedResponse.trailers().addAll(trailers);
278278
}
279279
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.tests.grpc;
12+
13+
import com.github.dockerjava.api.DockerClient;
14+
import com.github.dockerjava.api.command.WaitContainerCmd;
15+
import com.github.dockerjava.api.command.WaitContainerResultCallback;
16+
import io.netty.util.internal.PlatformDependent;
17+
import io.vertx.core.Vertx;
18+
import io.vertx.core.http.HttpClient;
19+
import io.vertx.core.http.HttpClientOptions;
20+
import io.vertx.core.http.HttpServer;
21+
import io.vertx.core.http.HttpServerOptions;
22+
import io.vertx.core.net.SocketAddress;
23+
import io.vertx.httpproxy.HttpProxy;
24+
import io.vertx.httpproxy.ProxyOptions;
25+
import org.junit.After;
26+
import org.junit.Before;
27+
import org.junit.BeforeClass;
28+
import org.junit.Test;
29+
import org.testcontainers.containers.GenericContainer;
30+
import org.testcontainers.containers.InternetProtocol;
31+
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
32+
import org.testcontainers.containers.wait.strategy.Wait;
33+
import org.testcontainers.images.builder.ImageFromDockerfile;
34+
35+
import java.time.Duration;
36+
37+
import static org.junit.Assert.assertEquals;
38+
import static org.junit.Assume.assumeFalse;
39+
40+
public class GrpcProxyIntegrationTest {
41+
42+
private static final int GRPC_SERVER_PORT = 50051;
43+
private static final int PROXY_PORT = 8080;
44+
45+
private Vertx vertx;
46+
private HttpServer proxyServer;
47+
private HttpClient httpClient;
48+
private ServerContainer<?> grpcServerContainer;
49+
private GenericContainer<?> grpcClientContainer;
50+
51+
@BeforeClass
52+
public static void beforeClass() throws Exception {
53+
assumeFalse("Cannot run Linux containers on Windows", PlatformDependent.isWindows());
54+
}
55+
56+
@Before
57+
public void setUp() throws Exception {
58+
vertx = Vertx.vertx();
59+
}
60+
61+
@After
62+
public void tearDownContainers() {
63+
if (grpcClientContainer != null) {
64+
grpcClientContainer.stop();
65+
}
66+
if (grpcServerContainer != null) {
67+
grpcServerContainer.stop();
68+
}
69+
if (proxyServer != null) {
70+
proxyServer.close().await();
71+
}
72+
if (httpClient != null) {
73+
httpClient.close().await();
74+
}
75+
if (vertx != null) {
76+
vertx.close().await();
77+
}
78+
}
79+
80+
@Test
81+
public void testGrpcThroughProxy() throws Exception {
82+
startGrpcServer();
83+
startProxy();
84+
int exitCode = runGrpcClient();
85+
assertEquals("gRPC client tests should pass", 0, exitCode);
86+
}
87+
88+
private void startProxy() {
89+
httpClient = vertx.createHttpClient(new HttpClientOptions()
90+
.setProtocolVersion(io.vertx.core.http.HttpVersion.HTTP_2)
91+
.setHttp2ClearTextUpgrade(false));
92+
93+
SocketAddress backend = SocketAddress.inetSocketAddress(grpcServerContainer.getMappedPort(GRPC_SERVER_PORT), grpcServerContainer.getHost());
94+
HttpProxy proxy = HttpProxy.reverseProxy(new ProxyOptions(), httpClient).origin(backend);
95+
96+
proxyServer = vertx.createHttpServer(new HttpServerOptions()
97+
.setPort(PROXY_PORT)
98+
.setHost("0.0.0.0")
99+
.setHttp2ClearTextEnabled(true))
100+
.requestHandler(proxy)
101+
.listen()
102+
.await();
103+
}
104+
105+
private void startGrpcServer() throws Exception {
106+
grpcServerContainer = new ServerContainer<>(new ImageFromDockerfile("vertx-http-proxy-grpc-server", false)
107+
.withFileFromClasspath("Dockerfile", "grpc/server/Dockerfile")
108+
.withFileFromClasspath("server.js", "grpc/server/server.js")
109+
.withFileFromClasspath("package.json", "grpc/package.json")
110+
.withFileFromClasspath("test.proto", "grpc/test.proto"));
111+
if (System.getProperties().containsKey("containerFixedPort")) {
112+
grpcServerContainer.withFixedExposedPort(GRPC_SERVER_PORT, GRPC_SERVER_PORT);
113+
} else {
114+
grpcServerContainer.withExposedPorts(GRPC_SERVER_PORT);
115+
}
116+
grpcServerContainer
117+
.withEnv("GRPC_PORT", String.valueOf(GRPC_SERVER_PORT))
118+
.withEnv("GRPC_HOST", "0.0.0.0")
119+
.waitingFor(Wait.forLogMessage(".*gRPC server listening.*", 1));
120+
121+
grpcServerContainer.start();
122+
}
123+
124+
private int runGrpcClient() throws Exception {
125+
grpcClientContainer = new GenericContainer<>(
126+
new ImageFromDockerfile("vertx-http-proxy-grpc-client", false)
127+
.withFileFromClasspath("Dockerfile", "grpc/client/Dockerfile")
128+
.withFileFromClasspath("client.js", "grpc/client/client.js")
129+
.withFileFromClasspath("package.json", "grpc/package.json")
130+
.withFileFromClasspath("test.proto", "grpc/test.proto"))
131+
.withNetworkMode("host")
132+
.withEnv("GRPC_SERVER", String.format("localhost:%d", PROXY_PORT))
133+
.withStartupCheckStrategy(new OneShotStartupCheckStrategy())
134+
.withStartupTimeout(Duration.ofMinutes(3));
135+
136+
grpcClientContainer.start();
137+
138+
DockerClient dockerClient = grpcClientContainer.getDockerClient();
139+
try (WaitContainerCmd cmd = dockerClient.waitContainerCmd(grpcClientContainer.getContainerId())) {
140+
return cmd.exec(new WaitContainerResultCallback())
141+
.awaitStatusCode();
142+
}
143+
}
144+
145+
private static class ServerContainer<SELF extends ServerContainer<SELF>> extends GenericContainer<SELF> {
146+
147+
public ServerContainer(java.util.concurrent.Future<String> dockerImageName) {
148+
super(dockerImageName);
149+
}
150+
151+
public SELF withFixedExposedPort(int hostPort, int containerPort) {
152+
super.addFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP);
153+
return self();
154+
}
155+
}
156+
}

src/test/java/module-info.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
requires io.vertx.testing.unit;
55
requires junit;
66
requires wiremock.standalone;
7+
requires testcontainers;
8+
requires com.github.dockerjava.api;
9+
requires org.apache.commons.lang3;
10+
requires io.netty.common;
711
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM fedora:43 AS base
2+
3+
RUN dnf install -y nodejs npm && dnf clean all
4+
5+
WORKDIR /app
6+
7+
COPY package.json ./
8+
RUN npm install
9+
10+
COPY test.proto ./
11+
12+
FROM base AS client
13+
14+
COPY client.js ./
15+
16+
CMD ["node", "client.js"]

0 commit comments

Comments
 (0)