Gateway
mctl gateway greet.proto writes an HTTP front door in front of a gRPC
service: a route table, a port to call the RPCs through, and the four answers a client
cares about.
What it reads
A proto's messages and services. Every unary RPC becomes a route; a streaming RPC has no route, because an HTTP response is one body with one end, and the generated file names the ones it left out rather than dropping them quietly.
syntax = "proto3";
package shop;
message CreateReq { string name = 1; }
message User { string id = 1; }
service UserService {
rpc Create(CreateReq) returns (User);
rpc List(CreateReq) returns (User);
rpc Watch(CreateReq) returns (stream User);
}
Routes
With no mapping the routes are mechanical — POST /<Service>/<Method>
— which is what you want on day one, before anyone has decided on URLs:
pub fn routes() -> Array[GatewayRoute] {
[
{ verb: "POST", path: "/UserService/Create", rpc: "/shop.UserService/Create" },
{ verb: "POST", path: "/UserService/List", rpc: "/shop.UserService/List" },
]
}
When the URLs are decided, put them in a file next to the proto — same name, extension
.routes — and the generator reads it instead:
# what the browser sees what the RPC is called post /v1/users /shop.UserService/Create get /v1/users /shop.UserService/List
pub fn routes() -> Array[GatewayRoute] {
[
{ verb: "POST", path: "/v1/users", rpc: "/shop.UserService/Create" },
{ verb: "GET", path: "/v1/users", rpc: "/shop.UserService/List" },
]
}
A line naming a method the proto does not declare is reported and skipped, so a typo is a message rather than a route that answers nothing.
The answers
| Status | When | Body |
|---|---|---|
200 | the upstream answered | the RPC's reply, as it came back |
400 | the request body is not JSON | {"error": "body is not JSON"} |
404 | no route answers that verb and path | {"error": "no route for ..."} |
502 | the upstream call failed | {"error": "upstream 500: ..."} |
pub fn Gateway::handle(
self : Gateway,
verb : String,
path : String,
body : Json,
) -> GatewayReply {
let target = method_of(verb, path)
if target == "" {
return { status: 404, body: error_body("no route for " + verb + " " + path) }
}
let reply = (self.caller.call)(target, body) catch {
e => return { status: 502, body: error_body(describe(e)) }
}
{ status: 200, body: reply }
}
The Caller port
The gateway does not link a gRPC client. It calls one function, which is the whole of its upstream:
pub(all) struct Caller {
call : (String, Json) -> Json raise GatewayError
}
Bind it to a channel, a pool, or a function that answers from a fixture, and the gateway
is live. handle takes a parsed body; handle_text takes the raw
one and answers 400 when it will not parse.
What it does not do yet
Routes are matched by verb and path exactly. A route written
get /v1/users/:id is emitted into the table, but a request for
/v1/users/7 will not match it, and the path parameter is not passed on to
the RPC. Until that is done, keep gateway routes literal.
mctl gateway also does not transcode protobuf to JSON: the body is forwarded
as the JSON document it arrived as, so the method behind call is expected to
take a JSON request. Bind it to a client that does the conversion, or to a service whose
transport is JSON.