Skip to content
Haskell library · RPC

peculiar-rpc.

gRPC, gRPC-Web and Connect. One Haskell binary. No proxy.

Browsers can't speak gRPC, so the usual answer is an Envoy sidecar translating in front of your server. peculiar-rpc does that translation inside the process: native gRPC, gRPC-Web and Connect clients all reach the same handlers on one port, mounted in your own WAI server beside your own routes. The handlers are derived from your .proto files, so a missing method is a missing field, and a renamed one doesn't compile.

Passes the official conformance suites. MIT licensed. Pure Haskell, no C libraries.

Main.hsimport Peculiar.Rpc qualified as Rpc
import Proto.Example.V1.Echo

Rpc.deriveService ''Echo

echo :: EchoServer
echo = EchoServer{say, count}

say :: Rpc.Context -> SayRequest -> IO SayResponse
say _ request = pure (defMessage & #text .~ request ^. #text)

main :: IO ()
main = Rpc.withServer Rpc.defaultCalls [Rpc.endpoint echo] \rpc ->
  Warp.run 8080 (Rpc.middleware rpc yourApp)

Conformance

Tested against the suites the protocols' own teams use.

The official Connect conformance suite and gRPC's own interop tests run against both the server and the client on every build, and every case passes.

10,204

of 10,204 server cases pass: gRPC, gRPC-Web and Connect, protobuf and JSON, over HTTP/1.1, h2c and TLS, mutual TLS and six compressions included

13,038

of 13,038 client cases pass: every protocol and both codecs over HTTP/2, the web ones over HTTP/1.1 too, with mutual TLS everywhere

64 / 64

gRPC interop runs against grpc-go: its client against this server and this client against its server, over h2c and TLS

0

proxies, sidecars or C libraries between a browser and your handler

The usual setup

Take the proxy out of the picture.

gRPC-Web and Connect differ from gRPC by a content type, where the trailers go, and how errors are written. That translation is small enough to own, and owning it removes a moving part from every deployment.

Before

A sidecar per service

Browser · gRPC-Web↓Envoy · translate↓Haskell · native gRPC only

Two processes to deploy, configure and keep in step. A reply without trailers reaches the browser as a phantom CORS error.

With peculiar-rpc

One binary, every client

Browser · gRPC-Web or ConnectMobile · native gRPCcurl · Connect unary↓Your Haskell server · one port

Every reply carries a gRPC status, failures included, so a version mismatch reads as UNIMPLEMENTED, not as a network fault.

What you get

Everything the wire needs, derived from the contract.

No JSON codec, on purpose: clients are generated from the same .proto files.

01

Derived servers and clients

One record per service with one field per RPC, its type fixed by the streaming kind. A missing method is a missing field.

02

Routes from the descriptor

Paths and Connect GET permissions are read from the service descriptor. Nothing written down, so nothing drifts.

03

gRPC-Web and Connect

gRPC-Web binary and text, Connect unary, GET and streaming, all served by the same handlers as native gRPC.

04

Limits after decompression

Receive limits are checked against the real message size, with gzip inflation capped one byte past the limit.

05

Deadlines from arrival

Enforced from when the request arrived, and every stream ends with a status, even one that timed out.

06

Mounts beside your routes

The service is a WAI application and middleware: add it to warp, servant or scotty and it shares the port with your own routes.

07

A client for every protocol

gRPC, Connect and gRPC-Web from one generated record, over HTTP/2 or HTTP/1.1, with mutual TLS, retries and interceptors.

08

Health and reflection

grpc.health.v1, and reflection that grpcurl reads, with descriptors derived from the code.

All of it in one package

Every protocol is served by the same code.

Native gRPC, the gRPC-Web and Connect translation, the HTTP/2 client and the unary fast path are all peculiar-rpc, on the http2 package. Owning the whole path made unary calls faster than connect-go and a 1000-message stream eight times faster than it was on a general-purpose gRPC library.