Skip to content
CommunicationDocs 1.3.x

Transport Matrix ​

Overview

Which RPC types work on which server transport. The Connect protocol states: "Bidirectional streaming requires HTTP/2, but the other RPC types also support HTTP/1.1" — a bidi service on an HTTP/1.1 transport does not fail at startup by itself; the first client send simply hangs forever (or the client receives HTTP 505). Connectum turns this into a startup diagnostic — see Startup validation below.

Server transport modes ​

createServer() picks the transport from tls and allowHTTP1:

ConfigurationTransportNode server
no tls, allowHTTP1: true (default)plaintext HTTP/1.1http.createServer
no tls, allowHTTP1: falseplaintext HTTP/2 (h2c)http2.createServer
tls configuredTLS + ALPN (HTTP/2; HTTP/1.1 also accepted when allowHTTP1: true)http2.createSecureServer

What a client without HTTP/2 sees over TLS ​

With tls and allowHTTP1: false, the listener serves HTTP/2 only, and what a client that cannot speak HTTP/2 sees depends on its ALPN offer:

  • A client that offers ALPN protocols but not h2 (for example only http/1.1) is refused at the TLS layer: the handshake fails with the no_application_protocol alert (ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL in Node.js).

  • A client that offers no ALPN at all completes the handshake, with no negotiated protocol. If it then sends HTTP/1.1, the server answers with a plaintext reply:

    text
    HTTP/1.0 403 Forbidden
    Content-Type: text/plain
    
    Missing ALPN Protocol, expected `h2` to be available.
    If this is a HTTP request: The server was not configured with the `allowHTTP1` option or a listener for the `unknownProtocol` event.

    The text comes from node:http2, not from Connectum, and the status is 403, not a TLS failure. Node.js 22.13.0, Node.js 26.12.1 and Bun 1.4.4 produce the same reply. A load balancer health check or a legacy client that does not offer ALPN hits this response, so point such probes at an HTTP/2 endpoint. With allowHTTP1: true the same client is served over HTTP/1.1.

RPC type support ​

TransportUnaryServer streamingClient streamingBidi streaming
Plaintext HTTP/1.1 (default)✅✅✅❌ blocked at startup
Plaintext h2c (allowHTTP1: false)✅✅✅✅
TLS + ALPN, HTTP/2 negotiated✅✅✅✅
TLS + ALPN, HTTP/1.1 negotiated✅✅✅❌ hangs at runtime

Residual risk: TLS with an HTTP/1.1 client

A TLS server with allowHTTP1: true is streaming-capable (HTTP/2 is negotiable), so startup validation does not hard-fail — but a client or intermediary that negotiates HTTP/1.1 over TLS (a client without h2 in its ALPN list, a proxy with an HTTP/1.1 upstream leg) hits the same silent hang on bidi calls. When bidi methods are present on such a server, Connectum logs a one-time warning at startup. Remove the risk entirely by setting allowHTTP1: false (the listener then serves HTTP/2 only), or keep bidi clients on HTTP/2 transports (createGrpcTransport, or createConnectTransport with httpVersion: "2"). Silence the warning with transportValidation: "off".

Pure gRPC protocol needs HTTP/2 even for unary

The matrix above is for the Connect protocol. The classic gRPC protocol (used by grpcurl, gRPC reflection clients, and createGrpcTransport) requires HTTP/2 for every RPC type — on the default plaintext HTTP/1.1 server, gRPC clients and grpcurl do not work at all. Use h2c or TLS.

Serving gRPC and HTTP/1.1 on one plaintext port ​

A single plaintext (no-TLS) port cannot serve both native gRPC (which needs HTTP/2 / h2c) and plain HTTP/1.1 clients. Per-connection protocol selection in the TLS mode uses ALPN. Node's cleartext servers do not provide automatic selection between an HTTP/1.1 request and an HTTP/2 connection preface. This is a limitation of the Node server APIs used by Connectum: Node core has declined to add cleartext allowHTTP1 (nodejs/node#26795, #44887 — both closed; maintainers prescribe userland byte-sniffing), and Upgrade: h2c is deprecated by RFC 9113. So createServer() offers allowHTTP1: true (HTTP/1.1 only) or false (h2c only) on a plaintext port — never both.

This matters when a reverse proxy / API gateway that speaks HTTP/1.1 (e.g. Ory Oathkeeper, nginx) fronts a service whose internal peers use native gRPC. Resolve it with one of these, in order of preference:

  1. Put a sidecar / edge proxy in front (recommended — runtime-agnostic). A proxy that multiplexes protocols — Envoy or Caddy — terminates the mixed edge and forwards a single protocol upstream. The proxy does the protocol detection the runtime cannot, and it works the same on every JS runtime (see the matrix below). See Envoy Gateway and Service Mesh.
  2. Use TLS + ALPN with allowHTTP1: true. A TLS server serves HTTP/1.1 and HTTP/2 on one port (ALPN negotiates per client). If app-level TLS is acceptable, this is the built-in mixed-port answer.
  3. Two listeners. Serve native gRPC (h2c) and Connect/HTTP-1.1 on separate ports/roles. Lower complexity, but not one port.

Connect and gRPC-Web do not need any of this

Only native gRPC needs HTTP/2. The Connect and gRPC-Web protocols run over HTTP/1.1, so the default plaintext HTTP/1.1 server already serves both — a gateway that downgrades to HTTP/1.1 works for them with no extra setup.

Runtime support for native gRPC ​

Native gRPC depends on HTTP/2 response trailers (grpc-status). The Bun Bun.serve API does not provide the HTTP/2 server behavior required by native gRPC in the measurements recorded below. Connectum does not use Bun.serve: createServer() builds on node:http2.

RuntimeNative gRPC serverConnect / gRPC-WebgRPC + HTTP/1.1 on one plaintext port
Node (node:http2 — what Connectum uses)✅✅❌ — use a sidecar proxy or TLS + ALPN
Bun (node:http2 — what Connectum uses)✅✅❌
Bun (Bun.serve)❌ (no HTTP/2 at all)*✅❌
Deno (Deno.serve)Not testedNot testedNot tested
Cloudflare WorkersNot supported by Connectum†Not supported by Connectum†Not supported by Connectum†

* Measured on Bun 1.3.13 and 1.3.14, and the reason is stronger than the missing trailers: Bun.serve has no HTTP/2 server at all. Offered ["h2","http/1.1"] over TLS it selects no protocol (alpnProtocol === false); an h2c prior-knowledge request fails with ERR_HTTP2_ERROR Protocol error; and Response has no trailer member, so a Trailer: header is echoed but nothing follows the body. There is no http2 option to enable -- Bun.serve silently ignores unknown keys, so passing one proves nothing. Bun 1.3.14 adds an HTTP/3 server and an experimental HTTP/2 client for fetch(); neither is an HTTP/2 server.

† Cloudflare Workers is a platform capability, not Connectum support — see Cloudflare Workers below.

This project has not tested the Deno row. It is left unclassified rather than inferred from a response API shape.

Connectum builds on neither API. Everything it does use is covered in Verified behaviour by runtime.

Takeaway: the measured limitation applies to Bun.serve; it does not apply to a Connectum server on Bun: createServer() builds on node:http2, whose server side delivers trailers on Bun as well — including plaintext h2c. Connect over HTTP/1.1 was verified on Node.js and Bun in the tested configurations below. If you write your own Bun.serve handler and must expose gRPC, terminate it at a sidecar proxy (Envoy / Caddy) and let the runtime serve Connect / HTTP-1.1. This project has not validated an equivalent deployment on Deno.

Bun client versions

Serving worked on the Bun versions recorded below, while Bun's node:http2 client only became usable in Bun 1.2.6 — see Runtime Compatibility.

Cloudflare Workers ​

createServer() is not a Cloudflare Workers entry point, and a Connectum server cannot be deployed to Workers today. createServer() builds its listener on node:http2 and node:http (createSecureServer, http2.createServer, http.createServer), and Cloudflare documents node:http2 in Workers as a non-functional stub that "can be imported or required, but does not provide a working implementation" (Node.js compatibility). Connectum has no Fetch-style handler for Workers. One is planned; nothing in this section describes shipped behavior.

What Cloudflare announces is a platform capability, not a Connectum guarantee. Its announcement describes a private beta in which a Worker speaks gRPC-web and the platform translates it to and from native gRPC. According to that post, Workers can serve unary and server-streaming gRPC APIs; it does not describe client streaming or bidirectional streaming for Workers, and presents full bidirectional streaming only for a gRPC server running in a container. This project has not run Connectum against that beta.

Verified behaviour by runtime ​

The following records historical measurements, not a new validation of the current peer-dependency line. Every row was run against a Connectum server built by createServer(), with @connectum/core 1.2.0 and @connectrpc/connect-node (both 2.0.0 and 2.1.2), asserting the response payload and the gRPC status code -- not merely that a call did not throw. Each scenario ran three times.

Server side -- createServer() ​

RuntimePlaintext HTTP/1.1 (default)Plaintext h2c (allowHTTP1: false)
Node.jsConnect unary ✅ · server streaming ✅ · error status ✅ · native gRPC ❌ (needs HTTP/2) · bidi ❌ refused at startupunary ✅ · server streaming ✅ · bidi ✅ · gRPC status in trailers ✅
Bun (1.1.38 and up)identical to Node.js, including the startup refusalunary ✅ · server streaming ✅ · bidi ✅ · gRPC status in trailers ✅

The startup refusal was run on both runtimes: a service with a bidi method on allowHTTP1: true rejects server.start() with CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT rather than starting and hanging on the first send.

Serving was never the Bun problem. A Connectum h2c server under Bun delivers HTTP/2 trailers in both directions -- verified down to Bun 1.1.38, including an error whose grpc-status arrives only in the trailer. This is because createServer() builds on node:http2; it is not affected by what Bun.serve can or cannot do.

Client side -- @connectrpc/connect-node ​

RuntimecreateGrpcTransport / createConnectTransport({httpVersion:'2'})createConnectTransport() (HTTP/1.1)
Node.jsunary ✅ · server streaming ✅ · bidi ✅ · trailers ✅unary ✅ · server streaming ✅ · bidi ❌ (protocol)
Bun <= 1.2.5❌ first RPC hangs -- the transport is constructed without error, the call never completes, nothing is thrownunary ✅ · server streaming ✅ · bidi ❌ (protocol)
Bun >= 1.2.6unary ✅ · server streaming ✅ · bidi ✅ · trailers ✅unary ✅ · server streaming ✅ · bidi ❌ (protocol)

The Bun boundary was located by bisection over 1.1.38, 1.2.0, 1.2.5, 1.2.6, 1.2.7, 1.2.8, 1.2.9, 1.2.10, 1.2.15, 1.2.21, 1.3.0, 1.3.13 and 1.3.14: 1.2.5 hangs, 1.2.6 passes, and every later version passes. Bun 1.2.6 rewrote the node:http2 client.

Re-checked on Bun 1.3.14 with no regressions, in-process and cross-process in both directions -- a Connectum server hosted by Bun answering a Node.js gRPC client, and the reverse. The cross-process runs captured a real DATA-then-TRAILERS frame sequence carrying grpc-status, so the trailer claim is a wire observation rather than an inference from a client library.

A hang, not an error

Earlier revisions of these docs described this as a TypeError. That symptom was not reproducible on any tested combination. What actually happens is worse to diagnose: the call simply never settles, so a service waits forever rather than failing fast. If you are pinned below Bun 1.2.6 and see a request that never returns, this is the first thing to check.

The two limitations are different in kind ​

Node.jsBun
Whatone plaintext port serves HTTP/1.1 or h2c, never boththe node:http2 client was unusable
Whyprotocol selection needs ALPN, which is a TLS handshake extension; a cleartext socket has no handshakeincomplete node:http2 implementation
Scopeserver sideclient side only
StatusNot supported by the Node cleartext APIs used here; see #26795 and #44887The recorded client boundary is Bun 1.2.6
Work around it byTLS + ALPN, or a sidecar proxyupgrading Bun

Bidirectional streaming in the Connect protocol requires HTTP/2, independently of the runtime. Connectum rejects a plaintext HTTP/1.1 configuration with mounted bidi methods -- see Startup validation.

Not tested ​

Stated so the table above is not read as broader than it is:

  • TLS + ALPN under Bun. Every run above was plaintext (h2c or HTTP/1.1).
  • Bun below 1.1.38.
  • Deno.serve, and Deno generally.
  • @connectrpc/connect-node 1.x (protobuf-es v1). Only 2.x was exercised, so the original TypeError report cannot be disproved for that generation.

Startup validation ​

When a mounted application service defines bidi-streaming methods and the effective transport is plaintext HTTP/1.1, server.start() rejects with a TransportValidationError carrying the stable code CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT, the affected service.method list, and both fixes. Only services passed to createServer() (and enabled) count: a bidi service that is merely declared in the same .proto file, or one contributed by a protocol such as Reflection, does not trip the check.

typescript
const server = createServer({
  services: [bidiRoutes],
  // no TLS + allowHTTP1 default → plaintext HTTP/1.1
});

await server.start();
// ✖ TransportValidationError [CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT]:
//   - acme.v1.ScannerService.StreamCodes (bidi_streaming)
//   Fix: allowHTTP1: false (h2c) or configure TLS.

Downgrade the check with transportValidation: "warn" (log once, start anyway) or "off" — for example behind an HTTP/2-terminating proxy where the bidi method is intentionally unused.

Learn More ​