Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement gRPC-Web protocol #89

Merged
merged 7 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
19 changes: 19 additions & 0 deletions .github/workflows/ci-5.x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ jobs:
jdk: ${{ matrix.jdk }}
os: ${{ matrix.os }}
secrets: inherit
gRPC-Web-Interop:
name: Run gRPC-Web interop tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout gRPC-Web
uses: actions/checkout@v2
with:
repository: grpc/grpc-web
ref: master
path: _grpc-web
- name: Install JDK
uses: actions/setup-java@v2
with:
java-version: 11
distribution: temurin
- name: Run tests
run: mvn -s .github/maven-ci-settings.xml -q clean verify -B -pl :vertx-grpc-server -am -Dgrpc-web.repo.path="$GITHUB_WORKSPACE/_grpc-web"
Deploy:
if: ${{ github.repository_owner == 'eclipse-vertx' && (github.event_name == 'push' || github.event_name == 'schedule') }}
needs: CI
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2011-2024 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.grpc.common;

import io.netty.util.AsciiString;
import io.vertx.codegen.annotations.Unstable;
import io.vertx.core.http.HttpHeaders;

/**
* The gRPC media types.
*/
@Unstable
public final class GrpcMediaType {

/**
* gRPC.
*/
public static final CharSequence GRPC = HttpHeaders.createOptimized("application/grpc");
/**
* gRPC with Protobuf message format.
*/
public static final CharSequence GRPC_PROTO = HttpHeaders.createOptimized("application/grpc+proto");

/**
* gRPC Web binary.
*/
public static final CharSequence GRPC_WEB = HttpHeaders.createOptimized("application/grpc-web");
/**
* gRPC Web binary with Protobuf message format.
*/
public static final CharSequence GRPC_WEB_PROTO = HttpHeaders.createOptimized("application/grpc-web+proto");

/**
* Whether the provided {@code mediaType} represents gRPC-Web
*
* @param mediaType the value to test
* @return {@code true} if the value represents gRPC-Web, {@code false} otherwise
*/
public static boolean isGrpcWeb(CharSequence mediaType) {
return AsciiString.regionMatches(GRPC_WEB, true, 0, mediaType, 0, GRPC_WEB.length());
}

/**
* gRPC Web text (base64).
*/
public static final CharSequence GRPC_WEB_TEXT = HttpHeaders.createOptimized("application/grpc-web-text");
/**
* gRPC Web text (base64) with Protobuf message format.
*/
public static final CharSequence GRPC_WEB_TEXT_PROTO = HttpHeaders.createOptimized("application/grpc-web-text+proto");

/**
* Whether the provided {@code mediaType} represents gRPC-Web
*
* @param mediaType the value to test
* @return {@code true} if the value represents gRPC-Web, {@code false} otherwise
*/
public static boolean isGrpcWebText(CharSequence mediaType) {
return AsciiString.regionMatches(GRPC_WEB_TEXT, true, 0, mediaType, 0, GRPC_WEB_TEXT.length());
}

private GrpcMediaType() {
// Constants
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ public Buffer payload() {
}

public static Buffer encode(GrpcMessage message) {
return encode(message, false);
}

public static BufferInternal encode(GrpcMessage message, boolean trailer) {
tsegismont marked this conversation as resolved.
Show resolved Hide resolved
ByteBuf bbuf = ((BufferInternal)message.payload()).getByteBuf();
int len = bbuf.readableBytes();
boolean compressed = !message.encoding().equals("identity");
ByteBuf prefix = Unpooled.buffer(5, 5);
prefix.writeByte(compressed ? 1 : 0); // Compression flag
prefix.writeByte((trailer ? 0x80 : 0x00) | (compressed ? 0x01 : 0x00));
tsegismont marked this conversation as resolved.
Show resolved Hide resolved
prefix.writeInt(len); // Length
CompositeByteBuf composite = Unpooled.compositeBuffer();
composite.addComponent(true, prefix);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public abstract class GrpcTestBase {
protected int port;

@Before
public void setUp() {
public void setUp(TestContext should) {
port = 8080;
vertx = Vertx.vertx();
}
Expand Down
35 changes: 35 additions & 0 deletions vertx-grpc-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.17.6</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -140,6 +146,35 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
<configuration>
<excludes>
<exclude>io/vertx/grpc/server/**/*ITest.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.5</version>
<executions>
<execution>
<id>interop-tests</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<includes>
<include>io/vertx/grpc/server/web/interop/InteropITest.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand Down
27 changes: 27 additions & 0 deletions vertx-grpc-server/src/main/asciidoc/server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ router.consumes("application/grpc").handler(rc -> grpcServer.handle(rc.request()
----
====

==== gRPC-Web protocol

The Vert.x gRPC Server supports the gRPC-Web protocol by default.

To disable the gRPC-Web protocol support, configure options with {@link io.vertx.grpc.server.GrpcServerOptions#setGrpcWebEnabled GrpcServerOptions#setGrpcWebEnabled(false)} and then create a server with {@link io.vertx.grpc.server.GrpcServer#server(io.vertx.core.Vertx, io.vertx.grpc.server.GrpcServerOptions) GrpcServer#server(vertx, options)}.

[TIP]
====
If your website server and the gRPC server are different, you have to configure the gRPC server for CORS.
This can be done with a Vert.x Web router and the CORS handler:

[source,java]
----
CorsHandler corsHandler = CorsHandler.create()
.addRelativeOrigin("https://www.mycompany.com")
.allowedHeaders(Set.of("keep-alive","user-agent","cache-control","content-type","content-transfer-encoding","x-custom-key","x-user-agent","x-grpc-web","grpc-timeout"))
.exposedHeaders(Set.of("x-custom-key","grpc-status","grpc-message"));
router.route("/com.mycompany.MyService/*").handler(corsHandler);
----
====

==== Request/response

Each service method is processed by a handler
Expand Down Expand Up @@ -86,6 +107,8 @@ A bidi request/response is simply the combination of a streaming request and a s
{@link examples.GrpcServerExamples#bidi}
----

NOTE: The gRPC-Web protocol does not support bidirectional streaming.

=== Flow control

Request and response are back pressured Vert.x streams.
Expand Down Expand Up @@ -113,10 +136,14 @@ You can compress response messages by setting the response encoding *prior* befo
{@link examples.GrpcServerExamples#responseCompression}
----

NOTE: Compression is not supported over the gRPC-Web protocol.

=== Decompression

Decompression is done transparently by the server when the client send encoded requests.

NOTE: Decompression is not supported over the gRPC-Web protocol.

=== Stub API

The Vert.x gRPC Server can bridge a gRPC service to use with a generated server stub in a more traditional fashion
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.vertx.grpc.server;

import io.vertx.core.json.JsonObject;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.impl.JsonUtil;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Base64;

/**
* Converter and mapper for {@link io.vertx.grpc.server.GrpcServerOptions}.
* NOTE: This class has been automatically generated from the {@link io.vertx.grpc.server.GrpcServerOptions} original class using Vert.x codegen.
*/
public class GrpcServerOptionsConverter {


private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;

static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, GrpcServerOptions obj) {
for (java.util.Map.Entry<String, Object> member : json) {
switch (member.getKey()) {
case "grpcWebEnabled":
if (member.getValue() instanceof Boolean) {
obj.setGrpcWebEnabled((Boolean)member.getValue());
}
break;
}
}
}

static void toJson(GrpcServerOptions obj, JsonObject json) {
toJson(obj, json.getMap());
}

static void toJson(GrpcServerOptions obj, java.util.Map<String, Object> json) {
json.put("grpcWebEnabled", obj.isGrpcWebEnabled());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2024 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 @@ -26,10 +26,10 @@
* <p> The server can be used as a {@link io.vertx.core.http.HttpServer} handler or mounted as a Vert.x Web handler.
*
* <p> Unlike traditional gRPC servers, this server does not rely on a generated RPC interface to interact with the service.
*
* <p>
* Instead, you can interact with the service with a request/response interfaces and gRPC messages, very much like
* a traditional client.
*
* <p>
* The server exposes 2 levels of handlers
*
* <ul>
Expand All @@ -42,12 +42,22 @@
public interface GrpcServer extends Handler<HttpServerRequest> {

/**
* Create a blank gRPC server
* Create a blank gRPC server with default options.
*
* @return the created server
*/
static GrpcServer server(Vertx vertx) {
return new GrpcServerImpl(vertx);
return server(vertx, new GrpcServerOptions());
}

/**
* Create a blank gRPC server with specified options.
*
* @param options the gRPC server options
* @return the created server
*/
static GrpcServer server(Vertx vertx, GrpcServerOptions options) {
return new GrpcServerImpl(vertx, options);
}

/**
Expand All @@ -60,7 +70,7 @@ static GrpcServer server(Vertx vertx) {
GrpcServer callHandler(Handler<GrpcServerRequest<Buffer, Buffer>> handler);

/**
* Set a service method call handler that handles any call call made to the server for the {@link MethodDescriptor} service method.
* Set a service method call handler that handles any call made to the server for the {@link MethodDescriptor} service method.
*
* @param handler the service method call handler
* @return a reference to this, so the API can be used fluently
Expand Down
Loading
Loading