Skip to content

Latest commit

 

History

64 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

grpc-webnext

Full bidirectional gRPC in the browser — real HTTP/2, REST, and WebSockets, on the same port as native gRPC.

CI crates.io npm crates.io Spec Conformance  ·  Rust · TypeScript · Go


grpc-webnext brings the complete gRPC experience to the browser — all four call types, deadlines, metadata, trailers, and cancellation — with no translation on the default path. The browser speaks real HTTP/2 (trailers, flow control, native multiplexing) tunneled over a WebSocket by h2ts, straight into an unmodified gRPC server. Want plaintext instead? Flip one switch for JSON over Fetch + WebSocket, or annotate a method for plain REST. One endpoint serves browsers, REST clients, and native gRPC clients — all on the same port.

import { makePromiseClient } from "@grpc-webnext/client";
import { GreeterDefinition } from "./gen/greeter.js";

const rpc = makePromiseClient(GreeterDefinition, { baseUrl: "https://api.example.com" });

const reply = await rpc.sayHello({ name: "world" });        // unary
for await (const tick of rpc.countdown({ from: 3 }))       // server-stream
for await (const msg of rpc.chat(source, { signal }))      // bidi, cancel via AbortSignal

✨ Features

  • Real gRPC, end to end. The default binary path is real HTTP/2 over h2ts into an unmodified server — no envoy-style translation, no lossy shim. Trailers, flow control, and stream multiplexing are the genuine article.
  • Same port as native gRPC. content-type disambiguates application/grpc from grpc-webnext; native gRPC clients pass through untouched. One listener, every audience.
  • Two codecs. Efficient binary protobuf, or plaintext JSON you can read in the browser's Network tab. Pick per client.
  • Two streaming transports. h2ts (one WebSocket, many multiplexed streams) or a custom Frame protocol (one WebSocket per stream) — configurable per client.
  • Raw REST, built in. Annotate methods with google.api.http (the grpc-gateway / Envoy standard) to expose real HTTP verbs and REST URLs with JSON bodies.
  • WebSockets in JSON or binary. Stream over a debuggable JSON WebSocket or a compact binary one — same semantics, your choice.
  • Full gRPC semantics. Unary · server-stream · client-stream · bidi, plus grpc-timeout deadlines, metadata (ASCII + -bin), canonical status codes, and cancellation.
  • In-process or proxy. Wrap a tonic Routes for a zero-hop native endpoint, or run the schema-agnostic proxy in front of any gRPC server, in any language.
  • Standard auth. No bespoke hooks — authorization is a per-RPC gRPC interceptor (in-process) or your mesh's ext_authz (proxy), uniform across every transport.
  • Polyglot, drift-proof. One wire format, implemented per language, held to a language-neutral conformance suite run over the real wire.
  • Familiar client API. Callback/EventEmitter and promise/async-iterable flavors, modeled on @grpc/grpc-js and Connect.

How it works

   Browser / Node clients            (TypeScript ✅ · Rust-WASM ⬜)
        │
        │   proto (default)          json / proto-ws
        │   real HTTP/2 ⤵ h2ts       custom Frame protocol ⤵
        ▼                            (Fetch unary · WebSocket streams)
   ┌───────────────────────────────────────────────────────────┐
   │  grpc-webnext endpoint — one port                          │
   │    in-process server:  Rust ✅ · Go ✅ · Node ⬜             │
   │    or standalone proxy (Rust) — front any gRPC upstream    │
   └───────────────────────────────────────────────────────────┘
        │   native application/grpc (same port, byte-for-byte passthrough)
        ▼
   Any gRPC server — tonic · grpc-go · grpc-java · …

Two worlds share one endpoint:

  • Binary (default) → real gRPC over h2ts. The browser runs a real HTTP/2 stack in TypeScript, tunneled over a WebSocket by h2ts; the server is unmodified tonic behind an h2ts gateway. No translation — trailers, multiplexing, and flow control are native.
  • JSON (and binary streaming: "ws") → the custom Frame protocol. Unary rides Fetch (the response body is [len│message][len│trailer], since browsers can't read HTTP trailers); streaming is one WebSocket per stream, one protobuf Frame per message (Subscribe · Message · HalfClose · Trailer · Reset · Header). Plaintext JSON keeps it debuggable in the browser.

Transport is a per-client config { codec, unary, streaming }:

Client config Unary Streaming On the wire
{ codec: "proto" }  (default) h2ts h2ts Real HTTP/2 over one WebSocket — multiplexed, unmodified gRPC
{ codec: "proto", unary: "fetch" } Fetch h2ts Fetch unary + multiplexed HTTP/2 streams
{ codec: "proto", streaming: "ws" } Fetch one WS / stream Custom Frame protocol — binary
{ codec: "json" } Fetch one WS / stream Custom Frame protocol — plaintext JSON
REST (server-annotated) any HTTP verb Plain JSON on /v1/… URLs — grpc-gateway-style

The wire format is defined once, in proto/grpc_webnext.proto and two normative documents — spec/PROTOCOL.md for the custom Frame path (Fetch unary + one WebSocket per stream) and spec/PROTOCOL_H2TS.md for the binary default, which is real gRPC over a tunnel and so mostly delegates to the HTTP/2 and gRPC specs. Every implementation is held to both by the conformance suite. See spec/COMPATIBILITY.md for per-transport gRPC-semantics fidelity.

Quickstart

Run the demo — starts the native Rust server and drives it from the TypeScript client:

cd node/packages/client && npm install && npm run demo

Use the clientnpm install @grpc-webnext/client; two flavors share one transport (add codec: "json" for plaintext):

import { makeClient, makePromiseClient } from "@grpc-webnext/client";
import { GreeterDefinition } from "./gen/greeter.js"; // ts-proto generic definitions

// Callback / EventEmitter — mirrors @grpc/grpc-js
const cb = makeClient(GreeterDefinition, { baseUrl: "https://api.example.com" });
cb.sayHello({ name: "world" }, (err, reply) => console.log(reply.message));

// Promise / async-iterable — mirrors Connect / nice-grpc
const rpc = makePromiseClient(GreeterDefinition, { baseUrl: "https://api.example.com" });
const reply = await rpc.sayHello({ name: "world" });          // unary → Promise
for await (const t of rpc.countdown({ from: 3 })) log(t);     // server-stream → AsyncIterable
const sum = await rpc.concat(source);                         // client-stream → Promise
for await (const m of rpc.chat(source, { signal })) log(m);   // bidi, cancel via AbortSignal

Wrap a tonic server in-process (serves grpc-webnext and native gRPC on one port):

use grpc_webnext::{bind_and_serve_in_process, ServerConfig};

let routes = tonic::service::Routes::new(GreeterServer::new(svc));
let (addr, handle) = bind_and_serve_in_process(routes, ServerConfig::default()).await?;

Front an existing gRPC server with the proxy (language-agnostic, no .proto required):

cd rust
UPSTREAM=http://localhost:50051 LISTEN=127.0.0.1:8080 cargo run -p grpc-webnext-proxy

Status

Component Location State
Wire protocol + normative spec proto/ · spec/PROTOCOL.md · spec/PROTOCOL_H2TS.md ✅ the contract
Rust server + proxy · crates.io rust/crates/grpc-webnext ✅ h2ts, custom Frame, +json, REST, deadlines, cancel, size limits, native same-port
TypeScript client (browser + Node) · npm node/packages/client ✅ h2ts + Fetch + WebSocket, typed codegen, callback + promise APIs
Conformance suite conformance/ ✅ language-neutral cases × Rust and Go servers × TS driver, run over the real wire
Go in-process server go/webnext ✅ h2ts, custom Frame, +json, REST, deadlines, cancel, size limits, native same-port
Node in-process server node/packages/server ⬜ skeleton
Rust client (WASM / frontend) · crates.io rust/crates/grpc-webnext-client ✅ h2ts, all four cardinalities, deadlines, reconnect + connectivity — no hyper/tokio, and tonic-free by default, with an optional feature that runs tonic's own generated stubs over the tunnel

Pre-1.0. The Rust server/proxy, the Go server, and the TypeScript client are covered by the conformance suite, which runs every case against both server implementations; the Node server is the remaining polyglot milestone. See doc/BACKLOG.md — including a plan to terminate grpc-webnext inside stock Envoy via a Rust dynamic-module filter, no sidecar.

Repository layout

Organized by language ecosystem — each toolchain owns its subtree — with the cross-language contract at the root.

proto/                 wire envelope (Frame, Metadatum, …) — source of truth
spec/                  PROTOCOL.md + PROTOCOL_H2TS.md (both normative) + COMPATIBILITY.md
conformance/           language-neutral conformance suite (proto, cases, driver)
doc/                   design notes (STATUS, BACKLOG, UNIFICATION, H2TS_INTEGRATION,
                       GO_SERVER, HTTPRULE_GAPS)

rust/                  Cargo workspace
  crates/grpc-webnext/   server library + proxy binary (grpc-webnext-proxy)
  examples/              Greeter service, end-to-end demo, conformance server

node/                  npm workspace
  packages/client/       @grpc-webnext/client — Fetch + WebSocket + h2ts transports
  packages/server/       @grpc-webnext/server — in-process (skeleton)

go/                    Go module (github.com/grpc-webnext/grpc-webnext/go)
  webnext/               in-process server — Fetch + WebSocket + h2ts + native gRPC
  examples/              Greeter service, conformance server

Development

cd rust && cargo test --workspace          # Rust: server + proxy
cd rust && cargo clippy --workspace --all-targets
cd go   && go test -race ./...             # Go: wire unit tests + end-to-end over real sockets
cd node && npm ci && npm test              # TypeScript: codec, e2e, and the conformance matrix

The conformance matrix runs every case against both server implementations, so the TypeScript suite needs a Rust and a Go toolchain — it builds and spawns both servers. Servers print LISTENING http://<addr> when ready.

License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Full bidirectional gRPC in the browser — real HTTP/2 tunneled over WebSockets via h2ts, plus REST and JSON, on the same port as native gRPC. Polyglot, conformance-tested, no translation on the default path.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages