Guide
Services and handlers
One record per service, one field per method, and a type for each field that the streaming kind decides.
What the splice writes
Rpc.deriveService ''Echo reads the method list proto-lens generated for Echo and writes two records and their instances. deriveServer and deriveClient write one half each.
| Declaration | What it is |
|---|---|
EchoServer |
A record with one handler field per RPC, named after it (say, count, …) |
instance Implementation EchoServer |
Turns the record into the server's handlers, and lists the message types reflection serves |
EchoClient |
A record with one calling field per RPC |
instance Client EchoClient |
Rpc.client options transport builds one |
Field names are the method names, so a module deriving services should enable DuplicateRecordFields (both records share them) and NoFieldSelectors (so a method called sum doesn't clash with Prelude.sum).
The four handler shapes
A handler's type is a type family of the method's streaming kind, so a streaming handler can't be given where a unary one is expected.
| Kind | Server field | Client field |
|---|---|---|
| Unary | Context -> i -> IO o |
i -> IO o |
| Server stream | Context -> i -> (o -> IO ()) -> IO () |
i -> (o -> IO ()) -> IO () |
| Client stream | Context -> IO (Maybe i) -> IO o |
((i -> IO ()) -> IO ()) -> IO o |
| Bidi stream | Context -> IO (Maybe i) -> (o -> IO ()) -> IO () |
((i -> IO ()) -> IO ()) -> (o -> IO ()) -> IO () |
Messages are plain proto-lens messages, not wrappers. On the server, IO (Maybe i) yields each request in turn and Nothing once the client has finished. On the client, the producer is handed a send and returns when it's done sending.
A unary method that receives no request message, or more than one, answers UNIMPLEMENTED before your handler runs.
The Context
data Context = Context
{ metadata :: Metadata
, timeout :: Maybe Timeout
, respondWith :: Metadata -> IO ()
, trailWith :: Metadata -> IO ()
, flush :: IO ()
, peer :: Maybe SockAddr
, requestPath :: ByteString
, requestCodec :: Codec
, requestBytes :: Maybe ByteString
}
metadata: the request's custom metadata. Read it withRpc.lookupHeader "authorization" context.metadata; names are matched lowercased, and-binvalues come back decoded.timeout: the deadline the caller set, if any. The server enforces it on its own; this is for handlers that want to budget work.respondWith: sets the response headers. It must run before the first message is sent.trailWith: adds trailers. They go out with the final status, including on errors.peer: the address of the connection the call arrived on, as the socket reports it: the TCP peer, under TLS too, or a Unix socket's address. It is the connection's own address and nothing else:X-Forwarded-ForandForwardedreach the handler as metadata, and whether to trust them is the application's decision. Two calls on one HTTP/2 connection see the same peer.flush: sends the headers now, before any message, so a client waiting on them isn't held up by a slow first response.requestPath: the method the call was made to, as/package.Service/Method, whichever protocol carried it. A REST call has the path of the method its route binds to.requestCodec:ProtoorJson, the encoding the request arrived in.requestBytes: for unary and server-streaming methods, the request message exactly as it arrived: decompressed and unframed, inrequestCodec, before it is decoded or validated. A REST call has the JSON built from its path, query and body. Client and bidi streams haveNothing.
The request is read before the server's interceptor runs, so an interceptor sees requestBytes too. A request signature over the path and the bytes can then be checked once, for every method:
verified :: Rpc.Interceptor
verified = Rpc.Interceptor \_ context continue ->
case (Rpc.lookupHeader "x-signature" context.metadata, context.requestBytes) of
(Just signature, Just bytes) | valid signature (context.requestPath <> bytes) -> continue context
_ -> Rpc.throwRpc Rpc.Unauthenticated "bad signature"
Checking against the bytes as sent matters: re-encoding a decoded message need not reproduce them, since field order and unknown fields are not preserved.
Errors
A handler fails by throwing an RpcError. The client, whichever protocol it speaks, receives the same code, message, google.rpc.Status details and trailing metadata.
lookupUser :: Rpc.Context -> LookupRequest -> IO LookupResponse
lookupUser _ request = do
found <- findUser (request ^. #id)
maybe (Rpc.throwRpc Rpc.NotFound "no such user") pure found
For details, build the error directly: Rpc.RpcError{status = Rpc.Status{code, message, details}, metadata}, where each Rpc.Detail is a type URL and the packed message.
To tell a retrying client when to come back, put Rpc.pushback (Rpc.seconds 2) in the error's metadata; Rpc.noRetry tells it not to. Both set grpc-retry-pushback-ms, the one grpc- name metadata may carry. A REST caller gets it as a Retry-After header, in whole seconds. Any other exception becomes UNKNOWN and goes to Calls.report. Its text is the message unless Calls.revealErrors is off.
Browser reachability
A browser's fetch can't stream a request body, so client-streaming and bidi methods are out of a browser's reach. Put Rpc.BrowserReachable Echo in a signature and the compiler rejects any such method, naming it:
webFacing :: (Rpc.BrowserReachable Echo) => EchoServer -> Rpc.Endpoint
webFacing = Rpc.endpoint
Connect over HTTP/2 does support client and bidi streaming for non-browser clients, so the check is something you choose to apply, not a rule of the server.