Skip to content

Commit

Permalink
gRPC transcoding
Browse files Browse the repository at this point in the history
Add support for gRPC transcoding
  • Loading branch information
vietj authored Feb 12, 2025
2 parents b553528 + f0a9e8c commit 608b657
Show file tree
Hide file tree
Showing 50 changed files with 4,627 additions and 29 deletions.
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@

<modules>
<module>vertx-grpc-common</module>
<module>vertx-grpc-transcoding</module>
<module>vertx-grpc-server</module>
<module>vertx-grpc-client</module>
<module>vertx-grpcio-common</module>
Expand Down Expand Up @@ -271,4 +272,4 @@
</plugins>
</build>

</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public GrpcClientResponseImpl(ContextInternal context,
long maxMessageSize,
GrpcStatus status,
HttpClientResponse httpResponse, GrpcMessageDecoder<Resp> messageDecoder) {
super(context, httpResponse, httpResponse.headers().get("grpc-encoding"), format, maxMessageSize, messageDecoder);
super(context, httpResponse, httpResponse.headers().get("grpc-encoding"), format, false, maxMessageSize, messageDecoder);
this.request = request;
this.httpResponse = httpResponse;
this.status = status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ public Buffer payload() {
private final GrpcMessageDecoder<T> messageDecoder;
private final Promise<Void> end;
private GrpcWriteStreamBase<?, ?> ws;
private boolean transcodable;

protected GrpcReadStreamBase(Context context,
ReadStream<Buffer> stream,
String encoding,
WireFormat format,
boolean transcodable,
long maxMessageSize,
GrpcMessageDecoder<T> messageDecoder) {
ContextInternal ctx = (ContextInternal) context;
Expand Down Expand Up @@ -95,12 +97,18 @@ protected void handleMessage(GrpcMessage msg) {
};
this.messageDecoder = messageDecoder;
this.end = ctx.promise();
this.transcodable = transcodable;
}

public void init(GrpcWriteStreamBase<?, ?> ws) {
this.ws = ws;
stream.handler(this);
stream.endHandler(v -> queue.write(END_SENTINEL));
stream.endHandler(v -> {
if (transcodable && last == null) {
handle(Buffer.buffer());
}
queue.write(END_SENTINEL);
});
stream.exceptionHandler(err -> {
if (err instanceof StreamResetException) {
StreamResetException reset = (StreamResetException) err;
Expand Down Expand Up @@ -267,6 +275,10 @@ public final void tryFail(Throwable err) {
}
}

protected final void cancelTranscodable() {
transcodable = false;
}

protected final void handleException(Throwable err) {
tryFail(err);
}
Expand Down
56 changes: 56 additions & 0 deletions vertx-grpc-docs/src/main/asciidoc/plugin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,59 @@ Vert.x gRPC needs to know to interact with gRPC.
- the message encoder

They can be used to bind services or interact with a remote server.

=== Generate transcoding definitions

The plugin can also generate transcoding definitions for the gRPC services. For more information see the
xref:server.adoc#_grpc_transcoding[Transcoding] section.

==== Generate transcoding definitions

Currently, the plugin supports generating transcoding definitions for the gRPC services via https://github.com/googleapis/api-common-protos/blob/main/google/api/http.proto[http.proto]. This feature is enabled by default.

Example of the gRPC transcoding definition:

[source,proto]
----
syntax = "proto3";
import "google/api/http.proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/hello"
};
}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/hello/{name}"
};
}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
----

To test if the transcoding is working correctly, you can use the `curl` command:

[source]
----
curl -X POST -H "Content-Type: application/json" -d '{"name":"vert.x"}' http://localhost:8080/v1/hello
----

And for the `SayHelloAgain` method:

[source]
----
curl -X POST -H "Content-Type: application/json" http://localhost:8080/v1/hello/vert.x
----

111 changes: 111 additions & 0 deletions vertx-grpc-docs/src/main/asciidoc/server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,117 @@ router.route("/com.mycompany.MyService/*").handler(corsHandler);
----
====

=== gRPC Transcoding

gRPC transcoding is a feature that enables mapping between HTTP/JSON requests and gRPC services.

==== What is gRPC Transcoding?

gRPC transcoding allows your services to accept both gRPC and HTTP/JSON requests, providing greater flexibility. This feature is particularly useful when:

* You want to expose your gRPC services to clients that don't support gRPC
* You need to support both traditional REST APIs and gRPC endpoints
* You want to leverage gRPC's efficiency while maintaining HTTP/JSON compatibility

==== Configuration

==== Enabling Transcoding

By default, gRPC transcoding is disabled in Vert.x. To enable it:

[source,java]
----
GrpcServerOptions options = new GrpcServerOptions().setGrpcTranscodingEnabled(true);
GrpcServer server = GrpcServer.server(vertx, options);
----

==== Creating Bindings

To create HTTP/gRPC bindings:

1. Create a `MethodTranscodingOptions` instance
2. Configure the binding options
3. Pass the options when registering the handler

[source,java]
----
import io.vertx.grpc.server.GrpcServer;
import io.vertx.grpc.server.GrpcServerOptions;
import io.vertx.grpc.transcoding.MethodTranscodingOptions;
import io.vertx.grpc.common.ServiceMethod;
import io.vertx.grpc.common.GrpcMessageDecoder;
import io.vertx.grpc.common.GrpcMessageEncoder;
import io.vertx.core.http.HttpMethod;
public class GrpcServerExamples {
// The encoder and decoder for the service method. Must be provided by the user and must be able to decode/encode the from wire format.
public static GrpcMessageDecoder<GetUserRequest> DECODER = GrpcMessageDecoder.json(GetUserRequest::newBuilder);
public static GrpcMessageEncoder<GetUserResponse> ENCODER = GrpcMessageEncoder.json();
public GrpcServer createTranscodingServer() {
// Create a new gRPC server with transcoding enabled
GrpcServer server = GrpcServer.server(vertx, new GrpcServerOptions().setGrpcTranscodingEnabled(true));
// Define the service method
ServiceMethod<GetUserRequest, GetUserResponse> getUser = ServiceMethod.server("UserService", "GetUser", ENCODER, DECODER);
// Define the transcoding options
MethodTranscodingOptions transcodingOptions = new MethodTranscodingOptions(null, HttpMethod.GET, "/v1/users/{id}", "*", null, null);
// Register the handler with transcoding options
server.callHandlerWithTranscoding(
getUser,
request -> {
// Handle the request
},
transcodingOptions
);
return server;
}
}
----

==== Testing Transcoding

To test gRPC transcoding, you can use a tool like `curl` to send HTTP requests to your gRPC service.

For example, to send a `GET` request to the `/v1/users/123` endpoint:

[source]
----
curl -X GET http://localhost:8080/v1/users/123
----

==== Error Handling

If an error occurs during transcoding, the server will return an HTTP error response with the appropriate status code.
Most grpc status codes are mapped to the corresponding HTTP status codes on best effort basis. If the status code is not mapped,
the server will return a 500 Internal Server Error.

|===
|gRPC Status Code | HTTP Status Code | Description
|OK | 200 | The operation completed successfully.
|CANCELLED | 408 | The operation was cancelled (typically by the caller).
|UNKNOWN | 500 | Unknown error.
|INVALID_ARGUMENT | 400 | Client specified an invalid argument.
|DEADLINE_EXCEEDED | 504 | Deadline expired before operation could complete.
|NOT_FOUND | 404 | Some requested entity (e.g., file or directory) was not found.
|ALREADY_EXISTS | 409 | Some entity that we attempted to create (e.g., file or directory) already exists.
|PERMISSION_DENIED | 403 | The caller does not have permission to execute the specified operation.
|RESOURCE_EXHAUSTED | 429 | Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.
|FAILED_PRECONDITION | 400 | Operation was rejected because the system is not in a state required for the operation's execution
|ABORTED | 409 | The operation was aborted, typically due to a concurrency issue like sequencer check failures, transaction aborts, etc.
|OUT_OF_RANGE | 400 | Operation was attempted past the valid range.
|UNIMPLEMENTED | 501 | Operation is not implemented or not supported/enabled in this service.
|INTERNAL | 500 | Internal errors. This means that some invariants expected by the underlying system have been broken.
|UNAVAILABLE | 503 | The service is currently unavailable.
|DATA_LOSS | 500 | Unrecoverable data loss or corruption.
|UNAUTHENTICATED | 401 | The request does not have valid authentication credentials for the operation.
|===

=== Server request/response API

The gRPC request/response server API provides an alternative way to interact with a client without the need of extending
Expand Down
12 changes: 12 additions & 0 deletions vertx-grpc-docs/src/main/java/examples/VertxGreeterGrpcServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.streams.ReadStream;
import io.vertx.core.streams.WriteStream;
Expand All @@ -13,6 +14,7 @@
import io.vertx.grpc.common.GrpcWriteStream;
import io.vertx.grpc.common.GrpcMessageDecoder;
import io.vertx.grpc.common.GrpcMessageEncoder;
import io.vertx.grpc.transcoding.MethodTranscodingOptions;
import io.vertx.grpc.server.GrpcServerResponse;
import io.vertx.grpc.server.GrpcServer;

Expand All @@ -32,6 +34,7 @@ public class VertxGreeterGrpcServer {
GrpcMessageEncoder.json(),
GrpcMessageDecoder.json(() -> examples.HelloRequest.newBuilder()));


public static class GreeterApi {

public Future<examples.HelloReply> sayHello(examples.HelloRequest request) {
Expand Down Expand Up @@ -75,6 +78,7 @@ public GreeterApi bind_sayHello(GrpcServer server, io.vertx.grpc.common.WireForm
return this;
}


public final GreeterApi bindAll(GrpcServer server) {
bind_sayHello(server);
return this;
Expand All @@ -84,5 +88,13 @@ public final GreeterApi bindAll(GrpcServer server, io.vertx.grpc.common.WireForm
bind_sayHello(server, format);
return this;
}

public final GreeterApi bindAllWithTranscoding(GrpcServer server) {
return this;
}

public final GreeterApi bindAllWithTranscoding(GrpcServer server, io.vertx.grpc.common.WireFormat format) {
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.streams.ReadStream;
import io.vertx.core.streams.WriteStream;
Expand All @@ -13,6 +14,7 @@
import io.vertx.grpc.common.GrpcWriteStream;
import io.vertx.grpc.common.GrpcMessageDecoder;
import io.vertx.grpc.common.GrpcMessageEncoder;
import io.vertx.grpc.transcoding.MethodTranscodingOptions;
import io.vertx.grpc.server.GrpcServerResponse;
import io.vertx.grpc.server.GrpcServer;

Expand Down Expand Up @@ -52,6 +54,7 @@ public class VertxStreamingGrpcServer {
GrpcMessageEncoder.json(),
GrpcMessageDecoder.json(() -> examples.Item.newBuilder()));


public static class StreamingApi {

public ReadStream<examples.Item> source(examples.Empty request) {
Expand Down Expand Up @@ -163,6 +166,7 @@ public final StreamingApi bind_pipe(GrpcServer server, io.vertx.grpc.common.Wire
return this;
}


public final StreamingApi bindAll(GrpcServer server) {
bind_source(server);
bind_sink(server);
Expand All @@ -176,5 +180,13 @@ public final StreamingApi bindAll(GrpcServer server, io.vertx.grpc.common.WireFo
bind_pipe(server, format);
return this;
}

public final StreamingApi bindAllWithTranscoding(GrpcServer server) {
return this;
}

public final StreamingApi bindAllWithTranscoding(GrpcServer server, io.vertx.grpc.common.WireFormat format) {
return this;
}
}
}
Loading

0 comments on commit 608b657

Please sign in to comment.