- 1. About this project
- 2. System Architecture
- 3. Data Flow
- 4. Adding gRPC-Rest-Gateway annotation to a protobuf file
- 5. Set up your project
- 6. Code generation library —
grpc-rest-gateway-code-gen - 7. Setting HTTP gateway server
- 8. Error handling and HTTP status code mapping
- 9. Protobuf to REST mapping
- 10. Run tests and sample app
- 11. Reference implementation using Apache Pekko
- 12. OpenAPI Specifications Configuration
- 13. Limitations
The gRPC-REST Gateway is a code generation tool and runtime library that automatically generates REST API endpoints from gRPC service definitions. It provides a reverse-proxy server that converts HTTP REST requests into gRPC calls, enabling dual-protocol access to backend services. This project is based on work done by grpcgateway.
The code generation module processes protobuf definitions and generates corresponding gateway handlers and OpenAPI specifications.
-
Gateway Generator: Creates HTTP-to-gRPC handler classes
-
OpenAPI Generator: Generates OpenAPI 3.1.0 specifications
-
Path Parser: Parses HTTP patterns and builds routing trees
-
Template Engine: Generates Scala code for different runtime implementations
+-------------+ +-----------------+ +----------------+ +------------------+
| | | | | | | |
| Proto Files |---->| Protobuf Parser |---->| Path Parser |---->| Code Templates |
| | | | | | | |
+-------------+ +-------+---------+ +-------+--------+ +--------+---------+
| | |
v v v
+----------------+ +----------------+ +------------------+
| | | | | |
| Service Model | | Routing Tree | | Generated Code |
| | | | | |
+----------------+ +----------------+ +------------------+
The runtime libraries provide the HTTP-to-gRPC bridge functionality with multiple implementation options.
Core abstractions and shared functionality:
-
GrpcGatewayHandler: Abstract base class for HTTP request dispatching
-
SwaggerHandler: OpenAPI specification serving and Swagger UI
-
Error Handling: gRPC to HTTP status code mapping
-
Request/Response Conversion: JSON to protobuf message conversion
Netty Implementation (runtime-netty)
-
Uses Netty HTTP server
-
Direct gRPC channel management
-
Synchronous request handling
Pekko Implementation (runtime-pekko)
-
Uses Apache Pekko HTTP
-
Asynchronous, actor-based processing
-
Route-based DSL for HTTP handling
Akka Implementation (runtime-akka)
-
Uses Akka HTTP
-
Similar to Pekko implementation
There are three main classes in each runtime implementation along with server classes to build REST server:
-
GrpcGatewayHandler— base abstract class / trait responsible for dispatching given HTTP request to the corresponding function in gRPC service. -
SwaggerHandler— responsible for Open API yaml files and generate Swagger UI. -
GatewayServer— responsible for starting HTTP server at the given host and port.
Main server component that orchestrates the gateway functionality:
-
HTTP Server: Handles incoming REST requests
-
gRPC Client: Communicates with backend gRPC services
-
Configuration Management: Host, port, TLS settings
-
Lifecycle Management: Server startup and shutdown
+----------------+ +---------------------+ +-------------------+
| | | | | |
| REST Client |<---->| gRPC-REST Gateway |<---->| gRPC Server |
| | | | | |
+----------------+ +--------+------------+ +--------+----------+
| |
| |
+--------v--------+ +--------v--------+
| | | |
| Swagger UI | | Business Logic |
| | | |
+-----------------+ +-----------------+
+----------------------+ +----------------------+ +----------------------+ | | | | | | | Code Generation | | Runtime Libraries | | Server Components | | | | | | | | +------------------+ | | +------------------+ | | +------------------+ | | | Gateway Generator| | | | Runtime Core | | | | Gateway Server | | | +------------------+ | | +------------------+ | | +------------------+ | | | OpenAPI Generator| | | | Runtime Netty | | | | Swagger Handler | | | +------------------+ | | +------------------+ | | +------------------+ | | | Annotations | | | | Runtime Pekko | | | | HTTP Handlers | | | +------------------+ | | +------------------+ | | +------------------+ | | | | | Runtime Akka | | | | | | | +------------------+ | | | +----------------------+ +----------------------+ +----------------------+
+-------------+ +----------------+ +-----------------+ +----------------+ +---------------+
| | | | | | | | | |
| HTTP Request|---->| Route Matching |---->| Request Parsing |---->| gRPC Call |---->| HTTP Response |
| | | | | | | | | |
+-------------+ +-------+--------+ +-------+---------+ +-------+--------+ +-------+-------+
| | | |
v v v v
+----------------+ +----------------+ +----------------+ +----------------+
| | | | | | | |
| Path/Method | | JSON to Proto | | gRPC Service | | Proto to JSON |
| Validation | | Conversion | | Invocation | | Conversion |
+----------------+ +----------------+ +----------------+ +----------------+
+----------------+ +----------------+ +----------------+ +----------------+
| | | | | | | |
| gRPC Error |---->| Status Mapping |---->| HTTP Status |---->| Error Response |
| Exception | | Table | | Code | | Body |
| | | | | | | |
+----------------+ +-------+--------+ +-------+--------+ +-------+--------+
| | |
v v v
+----------------+ +----------------+ +----------------+
| | | | | |
| gRPC Status | | HTTP Status | | JSON Error |
| to HTTP | | Code (400-500) | | Response |
| Mapping | | | | |
+----------------+ +----------------+ +----------------+
Each RPC must define the HTTP method and path using the google.api.http annotation, which can be done by importing google/api/annotations.proto.
So for the following RPC:
rpc GetRequest (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB)can be mapped to HTTP GET /restgateway/test/testserviceb by adding the following annotation:
rpc GetRequest (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB) {
option (google.api.http) = {
get: "/restgateway/test/testserviceb"
};
}The detail mapping between RPC and HTTP method can be found here.
Standard Google http annotations don’t provide a way to configure HTTP status code, default HTTP status code is mapped to 200 OK. The gRPC-rest gateway provides its own annotation to configure HTTP status code other than 200 OK.
Add the following dependency to your build.sbt:
"io.github.sfali23" %% "grpc-rest-gateway-annotations" % "0.9.1" % "compile,protobuf"The following is definition of annotation:
syntax = "proto3";
import "scalapb/scalapb.proto";
import "google/protobuf/descriptor.proto";
package grpc_rest_gateway.api;
option java_multiple_files = false;
option java_package = "com.improving.grpc_rest_gateway.api";
option java_outer_classname = "GrpcRestGatewayProto";
option (scalapb.options) = {
flat_package: true
single_file: true
retain_source_code_info: true
preserve_unknown_fields: false
package_name: "com.improving.grpc_rest_gateway.api"
};
message StatusDescription {
int32 status = 1; // Valid HTTP status code
string description = 2; // optional, description of given status
}
message Statuses {
StatusDescription successStatus = 1; // Default success status, default value is '200'
repeated StatusDescription otherStatus = 2; // Other status and their descriptions, this will be used in OpenApi documentation
}
extend google.protobuf.MethodOptions {
Statuses statuses = 50000;
}The following definition will configure default status to 204 NoContent.
option (grpc_rest_gateway.api.statuses) = {
successStatus: {
status: 204 // returns default status of No Content (204)
description: "Update resource"
},
// documentation of other statuses
otherStatus: [
{
status: 400
description: "Bad request"
},
{
status: 404
description: "Not found"
}
]
};The code-generator plugin has following options:
-
scala3Sources— Generate Scala 3 sources — It is used for using*as wild card import instead of_.-
For Scala 2.12 or later, set to true if
-Xsource:3scala compiler option is used. -
Always enabled if
useScala3Featuresflag is on.
-
-
useScala3Features— Whether to use Scala 3 features. It is used to useusingandgiveninstead ofimplicits. -
implementationType— The target implementation type (Netty, Pekko, Akka)
The OpenApi specs generator has following options:
-
version— the version of the OpenApi specification to generate, default value is0.1.0-SNAPSHOT.
See OpenAPI Specifications Configuration for more details.
To generate Scala classes for gateway handler and OpenApi specification, add following in plugin.sbt:
addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.8")
libraryDependencies ++= Seq(
"com.thesamet.scalapb" %% "compilerplugin" % "0.11.8",
"io.github.sfali23" %% "grpc-rest-gateway-code-gen" % "0.9.1"
)And following in the build.sbt:
Compile / PB.targets := Seq(
scalapb.gen() → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Netty) → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.openApiGen() → (Compile / resourceManaged).value / "specs"
)
// change the value of the "implementationType" parameter to grpc_rest_gateway.ImplementationType.Pekko or
// grpc_rest_gateway.ImplementationType.Akka to generate Pekko or Akka based implementation
// generate code for Scala 3
Compile / PB.targets := Seq(
scalapb.gen(scala3Sources = true) → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.gatewayGen(useScala3Features = true, implementationType = grpc_rest_gateway.ImplementationType.Pekko) → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.openApiGen() → (Compile / resourceManaged).value / "specs"
)
// Add the following to add openapi specs into the classpath
Compile / resourceGenerators += (Compile / PB.generate)
.map(.filter(.getName.endsWith("yml")))
.taskValueAdd the following dependencies
val AppVersion = "0.9.1"
val ScalaPb: String = scalapb.compiler.Version.scalapbVersion
val GrpcJava: String = scalapb.compiler.Version.grpcJavaVersion
val ScalaPbJson = "0.12.1"
libraryDependencies ++= Seq(
"io.github.sfali23" %% "grpc-rest-gateway-runtime-netty" % AppVersion,
"com.thesamet.scalapb" %% "compilerplugin" % ScalaPb % "compile;protobuf",
"com.thesamet.scalapb" %% "scalapb-runtime" % ScalaPb % "compile;protobuf",
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % ScalaPb,
"io.grpc" % "grpc-netty" % GrpcJava,
"com.thesamet.scalapb" %% "scalapb-json4s" % ScalaPbJson,
"com.thesamet.scalapb.common-protos" %% "proto-google-common-protos-scalapb_0.11" % "2.9.6-0" % "compile,protobuf",
// optional if using custom annotation
"io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion,
"io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion % "protobuf"
)Add the following dependencies:
val AppVersion = "0.9.1"
val ScalaPb: String = scalapb.compiler.Version.scalapbVersion
val GrpcJava: String = scalapb.compiler.Version.grpcJavaVersion
lazy val root = project
.in(file("."))
.enablePlugins(PekkoGrpcPlugin)
.settings(
libraryDependencies = Seq(
"io.github.sfali23" %% "grpc-rest-gateway-runtime-pekko" % AppVersion,
"org.apache.pekko" %% "pekko-actor" % "1.1.2",
"org.apache.pekko" %% "pekko-actor-typed" % "1.1.2",
"org.apache.pekko" %% "pekko-stream-typed" % "1.1.2",
"org.apache.pekko" %% "pekko-http" % "1.1.0",
"org.apache.pekko" %% "pekko-grpc-runtime" % "1.1.1",
"com.thesamet.scalapb.common-protos" %% "proto-google-common-protos-scalapb_0.11" % "2.9.6-0" % "compile,protobuf",
// optional if using custom annotation
"io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion,
"io.github.sfali23" %% "grpc-rest-gateway-annotations" % AppVersion % "protobuf"
),
pekkoGrpcGeneratedSources := generatedSource,
pekkoGrpcCodeGeneratorSettings := Seq("grpc", "single_line_to_proto_string"),
Compile / PB.targets = Seq(
grpc_rest_gateway
.gatewayGen(
scala3Sources = true,
implementationType = grpc_rest_gateway.ImplementationType.Pekko
) → crossTarget.value / "pekko-grpc" / "main",
grpc_rest_gateway.openApiGen() → (Compile / resourceManaged).value / "specs"
),
Compile / resourceGenerators += (Compile / PB.generate)
.map(.filter(.getName.endsWith("yml")))
.taskValue
)Code generation library is responsible for reading given Protobuf files and generating corresponding implementation of GrpcGatewayHandler based on its runtime library. The runtime handler can be generated by passing implementationType parameter:
There are three different plugins to generate runtime handlers, namely:
-
grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Netty)for Netty based implementation -
grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Pekko)for Pekko based implementation -
grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Akka)for Akka based implementation
|
Warning
|
Akka implementation hasn’t been tested yet due version dependency eviction in e2e testing module.
|
For example, the following Protobuf definition:
syntax = "proto3";
package rest_gateway_test.api;
import "scalapb/scalapb.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "common.proto";
import "grpc_rest_gateway/api/annotations.proto";
option java_multiple_files = false;
option java_package = "rest_gateway_test.api.java_api";
option java_outer_classname = "TestServiceBProto";
option objc_class_prefix = "TS2P";
option (scalapb.options) = {
single_file: true
lenses: true
retain_source_code_info: true
preserve_unknown_fields: false
flat_package: true
package_name: "rest_gateway_test.api.scala_api"
};
option (grpc_rest_gateway.api.openapi_info) = {
version: "1.0.0"
};
// Test service B
service TestServiceB {
rpc GetRequest (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB) {
option (google.api.http) = {
get: "/restgateway/test/testserviceb"
};
}
rpc Process (rest_gateway_test.api.model.TestRequestB) returns (rest_gateway_test.api.model.TestResponseB) {
option (google.api.http) = {
post: "/restgateway/test/testserviceb"
body: "*"
};
}
rpc Update (rest_gateway_test.api.model.TestRequestB) returns (google.protobuf.Empty) {
option (google.api.http) = {
put: "/restgateway/test/testserviceb/update"
body: "*"
};
option (grpc_rest_gateway.api.statuses) = {
successStatus: {
status: 204 // returns default status of No Content (204)
description: "Update resource"
},
// documentation of other statuses
otherStatus: [
{
status: 400
description: "Bad request"
},
{
status: 404
description: "Not found"
}
]
};
}
}openapi: 3.1.0
info:
version: 1.0.0
description: "REST API generated from TestServiceB.proto"
title: "TestServiceB.proto"
tags:
- name: TestServiceB
description: Test service B
paths:
/restgateway/test/testserviceb:
get:
tags:
- GetRequest
description: Generated from GetRequest
parameters:
- name: requestId
in: query
schema:
type: integer
format: int64
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/TestResponseB"
default:
description: Unexpected error
post:
tags:
- Process
description: Generated from Process
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/TestRequestB"
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/TestResponseB"
default:
description: Unexpected error
/restgateway/test/testserviceb/update:
put:
tags:
- Update
description: Generated from Update
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/TestRequestB"
responses:
"204":
description: Update resource
"400":
description: Bad request
"404":
description: Not found
default:
description: Unexpected error
components:
schemas:
TestRequestB:
type: object
properties:
requestId:
type: integer
format: int64
description: requestId
TestResponseB:
type: object
properties:
success:
type: boolean
request_id:
type: integer
format: int64
description: request_id
result:
type: string
description: result/*
* Generated by GRPC-REST gateway compiler. DO NOT EDIT.
*/
package rest_gateway_test.api.scala_api
import scalapb.GeneratedMessage
import io.grpc.ManagedChannel
import io.netty.handler.codec.http.{HttpMethod, QueryStringDecoder}
import com.improving.grpc_rest_gateway.runtime
import runtime.core.*
import runtime.handlers.*
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
object TestServiceBGatewayHandler {
private val GetGetRequestPath = "/restgateway/test/testserviceb"
private val PostProcessPath = "/restgateway/test/testserviceb"
private val PutUpdatePath = "/restgateway/test/testserviceb/update"
def apply(channel: ManagedChannel)(implicit ec: ExecutionContext): TestServiceBGatewayHandler =
new TestServiceBGatewayHandler(channel)
}
class TestServiceBGatewayHandler(channel: ManagedChannel)(implicit ec: ExecutionContext)
extends GrpcGatewayHandler(channel)(ec) {
import TestServiceBGatewayHandler.*
override val serviceName: String = "TestServiceB"
override val specificationName: String = "TestServiceB"
private lazy val client = TestServiceBGrpc.stub(channel)
override protected val httpMethodsToUrisMap: Map[String, Seq[String]] = Map(
"GET" -> Seq(
GetGetRequestPath
),
"POST" -> Seq(
PostProcessPath
),
"PUT" -> Seq(
PutUpdatePath
)
)
override protected def dispatchCall(method: HttpMethod, uri: String, body: String): Future[(Int, GeneratedMessage)] = {
val queryString = new QueryStringDecoder(uri)
val path = queryString.path
val methodName = method.name
if (isSupportedCall(HttpMethod.GET.name, GetGetRequestPath, methodName, path))
dispatchGetRequest(200, mergeParameters(GetGetRequestPath, queryString))
else if (isSupportedCall(HttpMethod.POST.name, PostProcessPath, methodName, path))
dispatchProcess(200, body)
else if (isSupportedCall(HttpMethod.PUT.name, PutUpdatePath, methodName, path))
dispatchUpdate(204, body)
else Future.failed(GatewayException.toInvalidArgument(s"No route defined for $methodName($path)"))
}
private def dispatchGetRequest(statusCode: Int, parameters: Map[String, Seq[String]]) = {
val input = Try {
val requestId = parameters.toLongValue("requestId")
rest_gateway_test.api.model.TestRequestB(requestId = requestId)
}
toResponse(input, client.getRequest, statusCode)
}
private def dispatchProcess(statusCode: Int, body: String) = {
val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
toResponse(input, client.process, statusCode)
}
private def dispatchUpdate(statusCode: Int, body: String) = {
val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
toResponse(input, client.update, statusCode)
}
}/*
* Generated by GRPC-REST gateway compiler. DO NOT EDIT.
*/
package rest_gateway_test.api.scala_api
import com.improving.grpc_rest_gateway.runtime
import runtime.core._
import runtime.handlers.GrpcGatewayHandler
import org.apache.pekko
import pekko.grpc.GrpcClientSettings
import pekko.actor.ClassicActorSystemProvider
import pekko.http.scaladsl.server.Route
import pekko.http.scaladsl.server.Directives._
import scala.concurrent.ExecutionContext
import scala.util.Try
class TestServiceBGatewayHandler(settings: GrpcClientSettings)(implicit sys: ClassicActorSystemProvider) extends GrpcGatewayHandler {
private implicit val ec: ExecutionContext = sys.classicSystem.dispatcher
private lazy val client = TestServiceBClient(settings)
override val specificationName: String = "TestServiceB"
override val route: Route = handleExceptions(exceptionHandler) {
pathPrefix("restgateway") {
pathPrefix("test") {
pathPrefix("testserviceb") {
concat(
pathEnd {
concat(
get {
parameterMultiMap { queryParameters =>
dispatchGetRequest(200, queryParameters)
}
},
post {
entity(as[String]) { body =>
dispatchProcess(200, body)
}
}
)
},
pathPrefix("update") {
pathEnd {
put {
entity(as[String]) { body =>
dispatchUpdate(204, body)
}
}
}
}
)
}
}
}
}
private def dispatchGetRequest(statusCode: Int, parameters: Map[String, Seq[String]]) = {
val input = Try {
val requestId = parameters.toLongValue("requestId")
rest_gateway_test.api.model.TestRequestB(requestId = requestId)
}
completeResponse(input, client.getRequest, statusCode)
}
private def dispatchProcess(statusCode: Int, body: String) = {
val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
completeResponse(input, client.process, statusCode)
}
private def dispatchUpdate(statusCode: Int, body: String) = {
val input = parseBody[rest_gateway_test.api.model.TestRequestB](body)
completeResponse(input, client.update, statusCode)
}
}
object TestServiceBGatewayHandler {
def apply(settings: GrpcClientSettings)(implicit sys: ClassicActorSystemProvider): GrpcGatewayHandler = {
new TestServiceBGatewayHandler(settings)
}
def apply(clientName: String)(implicit sys: ClassicActorSystemProvider): GrpcGatewayHandler = {
TestServiceBGatewayHandler(GrpcClientSettings.fromConfig(clientName))
}
}Implement your gRPC services as per your need and run gRPC server. Gateway server can be build and run as follows:
import com.improving.grpc_rest_gateway.runtime.server.GatewayServer
import rest_gateway_test.api.scala_api.TestServiceB.TestServiceBGatewayHandler
import scala.concurrent.ExecutionContext
implicit val ex: ExecutionContext = ??? // provide ExecutionContext
val server = GatewayServer(
serviceHost = "localhost",
servicePort = 8080, // assuming gRPC server is running on port 8080
gatewayPort = 7070, // REST end point is running at port 7070
toHandlers = channel => Seq(TestServiceBGatewayHandler(channel)),
executor = None, // Executor is useful if you want to allocate different thread pool for REST endpoint
usePlainText = true
)
server.start()
// stop server once done
server.stop()
// via Typesafe config
val mainConfig = ConfigFactory.load()
val server = GatewayServer(
config = mainConfig.getConfig("rest-gateway"),
toHandlers = channel => Seq(TestServiceBGatewayHandler(channel)),
executor = None
)Alternatively serviceHost, servicePort, gatewayPort, usePlainText can be overriden via environment variables GRPC_HOST, GRPC_SERVICE_PORT, REST_GATEWAY_PORT, and GRPC_USE_PLAIN_TEXT respectively.
// rest-gateway config is defined as follows:
rest-gateway {
host = "0.0.0.0"
host = ${?GRPC_HOST}
service-port = 8080
service-port = ${?GRPC_SERVICE_PORT}
gateway-port = 7070
gateway-port = ${?REST_GATEWAY_PORT}
use-plain-text = "true"
use-plain-text = ${?GRPC_USE_PLAIN_TEXT}
}Providing Pekko gRPC client configuration is defined as follows:
pekko {
grpc {
client {
pekko-gateway {
host = "0.0.0.0" // gRPC host
port = 8080 // grPC port
use-tls = false
}
}
}
}
// rest gateway config
rest-gateway {
host = "0.0.0.0"
host = ${?REST_GATEWAY_HOST}
port = 7070
port = ${?REST_GATEWAY_PORT}
hard-termination-deadline = 10.seconds // For Coordinated shutdown
hard-termination-deadline = ${?REST_GATEWAY_HARD_TERMINATION_DEADLINE}
}Gateway server can be initialized as follows:
implicit val system: ActorSystem[?] = ActorSystem[Nothing](Behaviors.empty, "grpc-rest-gateway-pekko")
val settings = GrpcClientSettings.fromConfig("pekko-gateway")
val config = system.settings.config
val restGatewayConfig = config.getConfig("rest-gateway")
GatewayServer(
restGatewayConfig,
TestServiceBGatewayHandler(settings)
).run()
// Or using HttSettings
GatewayServer(
HttpSettings(restGatewayConfig),
TestServiceBGatewayHandler(settings)
).run()Providing Akka gRPC client configuration is defined as follows:
akka {
grpc {
client {
pekko-gateway {
host = "0.0.0.0" // gRPC host
port = 8080 // grPC port
use-tls = false
}
}
}
}
// rest gateway config
rest-gateway {
host = "0.0.0.0"
port = 7070 // Gateway port
}Gateway server can be initialized as follows:
implicit val system: ActorSystem[?] = ActorSystem[Nothing](Behaviors.empty, "grpc-rest-gateway-pekko")
val settings = GrpcClientSettings.fromConfig("pekko-gateway")
val config = system.settings.config
GatewayServer(
config.getConfig("rest-gateway"),
TestServiceBGatewayHandler(settings)
).run()gRPC-REST gateway has built in mapping between gRPC and HTTP status codes. Following is the mappings between two systems:
| gRPC status code | HTTP status code |
|---|---|
OK |
OK (200) |
DATA_LOSS |
Partial Content (206) |
INVALID_ARGUMENT, OUT_OF_RANGE |
Bad Request (400) |
UNAUTHENTICATED |
Unauthorized(401) |
PERMISSION_DENIED |
Forbidden (403) |
NOT_FOUND, UNKNOWN |
Not Found (404) |
UNAVAILABLE |
Not Acceptable (406) |
ALREADY_EXISTS |
Conflict (409) |
ABORTED, CANCELLED |
Gone (410) |
FAILED_PRECONDITION |
Precondition Failed (412) |
INTERNAL |
Internal Server Error (500) |
UNIMPLEMENTED |
Not Implemented (501) |
DEADLINE_EXCEEDED |
Gateway Timeout (504) |
RESOURCE_EXHAUSTED |
Insufficient Storage (507) |
Note: Any unmapped code will be mapped to Internal Server Error (500).
Build io.grpc.StatusRuntimeException using io.grpc.protobuf.StatusProto to set corresponding status code and message in your implementation of gRPC server.
import com.google.rpc.{Code, Status}
import io.grpc.protobuf.StatusProto
import scala.concurrent.Future
// handle bad request
Future.failed(StatusProto.toStatusRuntimeException(
Status
.newBuilder()
.setCode(Code.INVALID_ARGUMENT_VALUE)
.setMessage("Invalid argument")
.build())
)
// not found
Future.failed(StatusProto.toStatusRuntimeException(
Status
.newBuilder()
.setCode(Code.NOT_FOUND_VALUE)
.setMessage("Not found")
.build())
)Following is how Protobuf to REST mapping will work as described in the documentation.
Given following Protobuf definition:
service Messaging {
rpc GetMessage(GetMessageRequest) returns (Message) {
option (google.api.http) = {
get: "/v1/messages/{message_id}/{sub.subfield}"
additional_bindings {
get: "/v1/messages/{message_id}"
}
};
}
rpc PostMessage(GetMessageRequest) returns (Message) {
option (google.api.http) = {
put: "/v1/messages/{message_id}"
body: "sub"
};
}
rpc PostMessage(GetMessageRequest) returns (Message) {
option (google.api.http) = {
post: "/v1/messages"
body: "*"
};
}
}
message GetMessageRequest {
message SubMessage {
string subfield = 1;
}
string message_id = 1;
SubMessage sub = 2;
}
message Message {
string text = 1;
}Following mapping defines how HTTP request supposed to be constructed.
HTTP method: GET
Path: /v1/messages/{message_id}/{sub.subfield}
HTTP request: http://localhost:7070/v1/messages/xyz/abc
Mapping: Both message_id and sub.subfield are mapped as path variables
HTTP method: GET
Path: /v1/messages/{message_id}
HTTP request: http://localhost:7070/v1/messages/xyz?sub.subfield=abc
Mapping: message_id is mapped as path variable while sub.subfield is mapped as query parameter
HTTP method: PUT
Path: |http://localhost:7070/v1/messages/xyz
HTTP request: http://localhost:7070/v1/messages/xyz?sub.subfield=abc [body: {"subfield": "sub"}]
Mapping: message_id is mapped as path variable while sub is mapped as body payload
HTTP method: POST
Path: /v1/messages
HTTP request: http://localhost:7070/v1/messages
Mapping: entire message is mapped as body payload
e2e module contains test code and a sample app.
Tests can be run as follows:
sbt "nettyJVM212Test"
sbt "nettyJVM213Test"
sbt "nettyJVM3Test"
sbt "pekkoJVM212Test"
sbt "pekkoJVM213Test"
sbt "pekkoJVM3Test"Sample app can be run as follows:
# For Scala 2.12
sbt "nettyJVM212Run"
sbt "pekkoJVM212Run"
# # For Scala 2.13
sbt "nettyJVM213Run"
sbt "pekkoJVM213Run"
# For Scala 3
sbt "nettyJVM3Run"
sbt "pekkoJVM3Run"Open browser and paste following URL in address bar http://localhost:7070, you should see Open API specification for service.
A reference implementation of Swagger petstore is attempted here. Follow steps described in README file to run reference implementation.
Open browser and paste following URL in address bar http://localhost:7070, you should see Open API specification for petstore service.
Following is corresponding proto file.
The gRPC-REST Gateway automatically generates OpenAPI 3.1.0 specifications from your protobuf definitions and serves them through the Swagger UI. This section explains how to configure the OpenAPI specs generator and customize the specs directory.
The OpenAPI specs generator accepts the following configuration options:
-
version— The version of the OpenAPI specification to generate. Default value is0.1.0-SNAPSHOT. This can be declared by setting theversionoption in the proto file.option (grpc_rest_gateway.api.openapi_info) = { version: "1.0.0" };
-
specsDirectory— The directory where OpenAPI specification files will be generated. This is configured at build time and runtime. The default value isspecs.
To generate OpenAPI specifications during the build process, add the openApiGen target to your build.sbt:
Compile / PB.targets := Seq(
scalapb.gen() → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Netty) → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.openApiGen() → (Compile / resourceManaged).value / "specs"
)
// Add the following to include OpenAPI specs in the classpath
Compile / resourceGenerators += (Compile / PB.generate)
.map(.filter(.getName.endsWith("yml")))
.taskValueIn this configuration:
-
OpenAPI specs are generated to
(Compile / resourceManaged).value / "specs"directory -
The
versionparameter sets the OpenAPI specification version (e.g.,"1.0.0") -
The
resourceGeneratorstask ensures the generated YAML files are included in the classpath
You can customize the output directory for OpenAPI specifications:
// Generate specs to a custom directory
Compile / PB.targets := Seq(
scalapb.gen() → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.gatewayGen(implementationType = grpc_rest_gateway.ImplementationType.Pekko) → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.openApiGen() → (Compile / resourceManaged).value / "openapi-specs"
)
// Include the custom directory specs in the classpath
Compile / resourceGenerators += (Compile / PB.generate)
.map(.filter(.getName.endsWith("yml")))
.taskValueThe runtime configuration for OpenAPI specifications is managed through Typesafe Config (HOCON format). The default configuration is defined in reference.conf:
openapi {
enabled = true
enabled = ${?OPENAPI_ENABLED}
specs-dir = "specs"
specs-dir = ${?OPENAPI_SPECS_FOLDER}
}Configuration properties:
-
enabled— Enable or disable OpenAPI/Swagger UI functionality. Default:true -
specs-dir— The directory name where OpenAPI specification files are located in the classpath. Default:"specs"
Both properties can be overridden using environment variables:
-
OPENAPI_ENABLED— Set totrueorfalse -
OPENAPI_SPECS_FOLDER— Set to your custom directory name
To use a custom specs directory, create or modify your application.conf:
openapi {
enabled = true
specs-dir = "my-custom-specs"
// or empty string to look for specs in the root of the classpath
// specs-dir = ""
}Or override via environment variable:
export OPENAPI_SPECS_FOLDER="my-custom-specs"|
Important
|
The specs-dir value must match the directory name where you generated the OpenAPI specs at build time (relative to the resource root).
|
Once configured, you can access the OpenAPI specifications through the following endpoints:
-
Swagger UI Landing Page:
http://localhost:7070
lazy val myService = project
.in(file("."))
.settings(
Compile / PB.targets := Seq(
scalapb.gen() → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.gatewayGen(
implementationType = grpc_rest_gateway.ImplementationType.Pekko
) → (Compile / sourceManaged).value / "scalapb",
grpc_rest_gateway.openApiGen() → (Compile / resourceManaged).value / "api-specs"
),
Compile / resourceGenerators += (Compile / PB.generate)
.map(.filter(.getName.endsWith("yml")))
.taskValue
)# gRPC service configuration
pekko {
grpc {
client {
my-service {
host = "localhost"
port = 8080
use-tls = false
}
}
}
}
# REST gateway configuration
rest-gateway {
host = "0.0.0.0"
host = ${?REST_GATEWAY_HOST}
port = 7070
port = ${?REST_GATEWAY_PORT}
hard-termination-deadline = 10.seconds
hard-termination-deadline = ${?REST_GATEWAY_HARD_TERMINATION_DEADLINE}
}
# OpenAPI configuration
openapi {
enabled = true
enabled = ${?OPENAPI_ENABLED}
specs-dir = "api-specs"
specs-dir = ${?OPENAPI_SPECS_FOLDER}
}If Swagger UI is not loading, verify:
-
OpenAPI is enabled: Check that
openapi.enabled = truein your configuration -
Specs directory matches: Ensure the
specs-dirinapplication.confmatches the directory used inbuild.sbt -
Specs are in classpath: Verify that
resourceGeneratorstask is configured correctly -
YAML files exist: Check that
.ymlfiles were generated during compilation
If YAML files are not being generated:
-
Check build configuration: Ensure
openApiGen()is included inPB.targetsCompile / PB.targets := Seq( scalapb.gen() -> (Compile / sourceManaged).value / "scalapb", grpc_rest_gateway.gatewayGen(...) -> (Compile / sourceManaged).value / "scalapb", grpc_rest_gateway.openApiGen() -> (Compile / resourceManaged).value / "specs" )
-
Verify proto files have HTTP annotations: Only services with
google.api.httpannotations generate YAMLrpc GetRequest (TestRequest) returns (TestResponse) { option (google.api.http) = { get: "/api/resource" }; } -
Check compilation: Run
sbt compileand look for protobuf generation messages
If YAML files are generated but not accessible at runtime:
-
Verify resourceGenerators: Ensure YAML files are included in classpath
Compile / resourceGenerators += (Compile / PB.generate) .map(_.filter(_.getName.endsWith("yml"))) .taskValue
-
Check resource directory: YAML files should be in
target/…/classes/specs/
If the version in generated YAML doesn’t match your proto file’s openapi_info:
-
Verify import: Ensure you import the annotations proto file
import "grpc_rest_gateway/api/annotations.proto";
-
Check option syntax: Verify the
openapi_infooption is correctly formattedoption (grpc_rest_gateway.api.openapi_info) = { version: "1.0.0" };
-
Rebuild: Clean and recompile to ensure changes are picked up
sbt clean compile

