gRPC HTTP transcoding
This extension is Java implementation of gRPC HTTP transcoding, like grpc-gateway in Go.
Read google.api.http specification to learn how to define the HTTP mapping.
Why Not Sidecar
Why not just use a sidecar like Envoy gRPC-JSON transcoder to do the transcoding?
- Sidecar is like a black box and is inconvenient for debugging and testing
- Scalability is not enough, and transcoding customization is challenging.
Dependencies
Choose the appropriate dependencies based on your 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>
Because both webflux and gRPC use netty, if you use the grpc-netty-shaded
dependency, the final jar package will be larger (~9MB).
Therefore, 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
-
When the application starts up, it will get all the protobuf configurations for HTTP mapping (google.api.http), and then register them into the internally implemented
RouterFunction
. -
When an HTTP request comes in, the
RouterFunction
will find the corresponding gRPC service based on the request’s URL and method, then convert the HTTP request into a gRPC request, send it to the gRPC server endpoint, and finally convert the gRPC response back into an HTTP response to return.
Refer to grpc-gateway for more details.