gRPC HTTP Transcoding
This extension provides a Java implementation of gRPC HTTP transcoding, similar to grpc-gateway in Go.
Refer to the google.api.http specification to understand how to define HTTP mappings.
Why Not Use a Sidecar?
Why not use a sidecar like Envoy gRPC-JSON transcoder for transcoding?
- A sidecar acts as a black box, making debugging and testing inconvenient.
- It lacks sufficient scalability, and customizing transcoding is challenging.
Dependencies
Select the dependencies that match your technology stack.
- WebMVC
- WebFlux
- Gradle
- Maven
implementation("io.github.danielliu1123:grpc-server-boot-starter")
implementation("io.github.danielliu1123:grpc-starter-transcoding")
implementation("org.springframework.boot:spring-boot-starter-web")
<dependency>
<groupId>io.github.danielliu1123</groupId>
<artifactId>grpc-server-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.github.danielliu1123</groupId>
<artifactId>grpc-starter-transcoding</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- Gradle
- Maven
implementation("io.github.danielliu1123:grpc-server-boot-starter")){
exclude(group: "io.grpc", module: "grpc-netty-shaded")
}
implementation("io.github.danielliu1123:grpc-starter-transcoding")
implementation("org.springframework.boot:spring-boot-starter-webflux")
runtimeOnly("io.grpc:grpc-netty")
<dependency>
<groupId>io.github.danielliu1123</groupId>
<artifactId>grpc-server-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.danielliu1123</groupId>
<artifactId>grpc-starter-transcoding</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<scope>runtime</scope>
</dependency>
Since both WebFlux and gRPC utilize Netty, using the grpc-netty-shaded
dependency will increase the size of the final JAR package by approximately 9MB.
To avoid this, exclude the grpc-netty-shaded
dependency and use the grpc-netty
dependency instead.
Example
Transcoding is only support unary and server streaming RPCs.
Unary
syntax = "proto3";
import "google/api/annotations.proto";
package transcoding.mvc;
message SimpleRequest {
string requestMessage = 1;
}
message SimpleResponse {
string responseMessage = 1;
}
service SimpleService {
rpc UnaryRpc (SimpleRequest) returns (SimpleResponse) {
option (google.api.http) = {
post: "/unary",
body: "*"
};
}
}
HTTP:
curl -X POST -d '{"requestMessage": "World"}' http://localhost:8080/unary
gRPC:
grpcurl -plaintext -d '{"requestMessage": "World"}' localhost:9090 transcoding.mvc.SimpleService/UnaryRpc
When grpc.transcoding.auto-mapping
is true
(default value) and google.api.http
option is not specified,
the default mapping path is POST /<package>.<Service>/<MethodName>
(case-sensitive).
Refer to webmvc example.
Server Streaming
It is implemented using SSE (Server-Sent Events), data will be returned in JSON format.
syntax = "proto3";
import "google/api/annotations.proto";
package transcoding.mvc;
message SimpleRequest {
string requestMessage = 1;
}
message SimpleResponse {
string responseMessage = 1;
}
service SimpleService {
rpc ServerStreamingRpc (SimpleRequest) returns (stream SimpleResponse) {
option (google.api.http) = {
get: "/serverstreaming"
};
}
}
HTTP:
curl http://localhost:8080/serverstreaming?requestMessage=World
gRPC:
grpcurl -plaintext -d '{"requestMessage": "World"}' localhost:9090 transcoding.mvc.SimpleService/ServerStreamingRpc
Refer to webmvc example.
The values of request message fields can be filled by path variables, request body and query parameters. If the same field exists, the priority is: path variables > request body > query parameters.
Extension Points
HeaderConverter
HeaderConverter
is used to
- Convert HTTP headers to gRPC metadata when transcode from HTTP to gRPC.
- Convert gRPC metadata to HTTP headers when transcode from gRPC to HTTP.
The default implementation is DefaultHeaderConverter
.
- When converting HTTP headers to gRPC metadata (request), it removes all http canonical headers (except Authorization) and leaves only custom headers.
- When converting gRPC metadata to HTTP headers (response), it removes all http canonical headers, headers with the prefix
grpc-
, headers with the suffix-bin
and leaves only custom headers.
Use custom HeaderConverter
to replace the default implementation:
@Component
public class CustomHeaderConverter implements HeaderConverter {
@Override
public Metadata toMetadata(HttpHeaders headers) {
// convert HttpHeaders to Metadata
}
@Override
public HttpHeaders toHttpHeaders(Metadata headers) {
// convert Metadata to HttpHeaders
}
}
Custom Transcoding Exception Handling
Provides two interfaces TranscodingExceptionResolver
(Spring MVC)
and ReactiveTranscodingExceptionHandler
(Spring WebFlux) for custom transcoding exception handling.
The default implementations are DefaultTranscodingExceptionResolver
and DefaultReactiveTranscodingExceptionHandler
.
The logic of the default implementation is:
when an exception occurs, gRPC status code will be converted to http status code,
and then throw TranscodingRuntimeException
,
which will then be captured and handled by Spring MVC/Flux.
If you want Spring to handle ResponseStatusException
,
you need to set spring.{mvc,webflux}.problemdetails.enabled
to true
.
TranscodingExceptionResolver
Customize a TranscodingExceptionResolver
to replace the default implementation:
@Component
public class MyTranscodingExceptionResolver implements TranscodingExceptionResolver {
@Override
public ServerResponse resolve(StatusRuntimeException exception) {
// do your custom logic here ...
}
}
Spring MVC does not handle ResponseStatusException
thrown by HandlerFunction
before Spring 6.2.0
.
Refer to spring-projects/spring-framework#32689.
ReactiveTranscodingExceptionHandler
Customize a ReactiveTranscodingExceptionHandler
to replace the default implementation:
public class MyReactiveTranscodingExceptionResolver implements ReactiveTranscodingExceptionResolver {
@Override
public void resolve(MonoSink<ServerResponse> sink, StatusRuntimeException exception) {
// do your custom logic here ...
// return a exception response
// sink.error(new MyException());
// return a normal response
// ServerResponse.ok().bodyValue("Ops!").subscribe(sink::success);
}
}
Configurations
Disable transcoding:
grpc:
transcoding:
enabled: false
Disable auto-mapping:
grpc:
transcoding:
auto-mapping: false
Custom gRPC server endpoint:
grpc:
transcoding:
endpoint: localhost:9090
In most cases, you do not need to manually configure this property, it will automatically find the gRPC server endpoint of the current application.
How It Works
-
During application startup, all protobuf configurations for HTTP mapping (google.api.http) are retrieved and registered into the internally implemented
RouterFunction
. -
When an HTTP request is received, the
RouterFunction
identifies the corresponding gRPC service based on the request’s URL and method. It then converts the HTTP request into a gRPC request, sends it to the gRPC server endpoint, and finally converts the gRPC response back into an HTTP response to return to the client.
For more details, refer to grpc-gateway.