Skip to content
TypeWire

@tahanabavi/typefetch-grpc

v0.1.0

gRPC transport for TypeFetch contracts — Connect unary JSON out of the box, binary grpc-web behind a codec seam, zero dependencies.

Errors

Failures normalise onto typefetch's shared ErrorKind, so one global handler covers every transport:

ts
client.onError((error) => {
if (error.kind === 'unauthenticated') redirectToLogin()
})

That fires for a gRPC UNAUTHENTICATED exactly as it does for an HTTP 401 and a GraphQL UNAUTHENTICATED.

The typed error body stays gRPC-native: a gRPC endpoint's errors map is keyed by the numeric gRPC code, not the HTTP status.

ts
import { GrpcCode } from "@tahanabavi/typefetch-grpc";

getUser: {
transport: "grpc",
service: "user.v1.UserService",
rpc: "GetUser",
request: z.object({ id: z.string() }),
response: User,
errors: {
[GrpcCode.NotFound]: z.object({ code: z.string(), message: z.string() }),
},
}
ts
if (isContractError(contracts.user.getUser, e, GrpcCode.NotFound)) {
e.data.message // typed from the schema above
}

Keying on the code rather than the status is what makes a contract's declared errors mean the same thing over Connect (which maps codes onto real statuses) and over binary grpc-web (which answers 200 and reports the code in a trailer). RichError.grpcCode carries the numeric code; RichError.status stays the HTTP status.

Binary grpc-web

Reach for this only when you need protobuf on the wire. Connect JSON is debuggable in a way binary frames never are.

ts
import { grpcTransport } from '@tahanabavi/typefetch-grpc'
import { GetUserRequest, GetUserResponse } from './gen/user_pb'

grpcTransport({
codec: {
encode: (message) => GetUserRequest.toBinary(message),
decode: (bytes) => GetUserResponse.fromBinary(bytes),
},
})

The codec deals only in message bytes. This package owns the 5-byte length-prefix framing, the 0x80 trailer frame, percent-decoding of grpc-message, and trailers-only responses — protocol concerns, not schema ones. That boundary is what keeps protobuf out of everything published here.

Set a codec per endpoint to mix binary and JSON RPCs on one client.

The failure everyone hits first

A browser calling a cross-origin grpc-web server needs the server to expose the trailer:

txt
Access-Control-Expose-Headers: grpc-status, grpc-message

Without it the trailer is invisible to the client even though it was sent, and every call fails. This transport detects that exact case and says so in the error message rather than reporting a generic parse failure.

Binary grpc-web also generally needs a proxy (Envoy, or a server with grpc-web support built in) in front of a plain gRPC server. grpc-web-text (base64) is not supported.