diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index b15c30e..27b416c 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -128,6 +128,7 @@ const guideSidebar: DefaultTheme.SidebarItem[] = [ collapsed: true, items: [ { text: 'Architecture Patterns', link: '/en/guide/production/architecture' }, + { text: 'Transport Matrix', link: '/en/guide/production/transport-matrix' }, { text: 'Docker', link: '/en/guide/production/docker' }, { text: 'Kubernetes', link: '/en/guide/production/kubernetes' }, { text: 'Envoy Gateway', link: '/en/guide/production/envoy-gateway' }, diff --git a/en/guide/production/transport-matrix.md b/en/guide/production/transport-matrix.md new file mode 100644 index 0000000..dc02456 --- /dev/null +++ b/en/guide/production/transport-matrix.md @@ -0,0 +1,80 @@ +--- +outline: deep +--- + +# Transport Matrix + +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](#startup-validation) below. + +## Server transport modes + +`createServer()` picks the transport from `tls` and `allowHTTP1`: + +| Configuration | Transport | Node server | +|---|---|---| +| no `tls`, `allowHTTP1: true` (**default**) | plaintext HTTP/1.1 | `http.createServer` | +| no `tls`, `allowHTTP1: false` | plaintext HTTP/2 (h2c) | `http2.createServer` | +| `tls` configured | TLS + ALPN (HTTP/2 and HTTP/1.1 negotiated) | `http2.createSecureServer` | + +## RPC type support + +| Transport | Unary | Server streaming | Client streaming | Bidi 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 | + +::: warning 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 server then refuses HTTP/1.1 at ALPN, so HTTP/1.1 +clients fail the handshake explicitly instead of hanging on bidi), or keep bidi +clients on HTTP/2 transports (`createGrpcTransport`, or `createConnectTransport` +with `httpVersion: "2"`). Silence the warning with `transportValidation: "off"`. +::: + +::: tip 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. +::: + +## Startup validation + +When a registered 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: + +```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 + +- [Security & TLS](/en/guide/security) — TLS configuration, mTLS +- [Server Configuration](/en/guide/server/configuration) — `createServer()` options diff --git a/en/guide/security.md b/en/guide/security.md index 142622f..8014675 100644 --- a/en/guide/security.md +++ b/en/guide/security.md @@ -23,7 +23,7 @@ const server = createServer({ await server.start(); ``` -When TLS is configured, Connectum creates an HTTP/2 secure server (`http2.createSecureServer`). Without TLS, it creates a plaintext HTTP/2 server. +When TLS is configured, Connectum creates an HTTP/2 secure server (`http2.createSecureServer`) with ALPN negotiation. Without TLS, the default server is **plaintext HTTP/1.1** (`http.createServer`, since `allowHTTP1` defaults to `true`); set `allowHTTP1: false` to get a plaintext HTTP/2 (h2c) server instead. See the [transport matrix](/en/guide/production/transport-matrix) for which RPC types each transport supports. ## Key Concepts @@ -32,7 +32,7 @@ When TLS is configured, Connectum creates an HTTP/2 secure server (`http2.create | **TLS Options** | `keyPath` + `certPath` for explicit paths, or `dirPath` for directory-based config | | **Environment Variables** | `TLS_DIR_PATH`, `TLS_KEY_PATH`, `TLS_CERT_PATH` for deployment flexibility | | **mTLS** | Mutual TLS via `http2Options`: `requestCert`, `rejectUnauthorized`, `ca` | -| **HTTP/2** | Default transport; `allowHTTP1: true` enables HTTP/1.1 fallback for ConnectRPC | +| **Transports** | Without TLS: HTTP/1.1 by default, h2c with `allowHTTP1: false`; with TLS: ALPN (HTTP/2 + HTTP/1.1). See the [transport matrix](/en/guide/production/transport-matrix) | | **Utility Functions** | `readTLSCertificates()`, `getTLSPath()` from `@connectum/core` | ## Learn More diff --git a/en/packages/core.md b/en/packages/core.md index d71cc1a..a12b7b0 100644 --- a/en/packages/core.md +++ b/en/packages/core.md @@ -77,10 +77,11 @@ The server is created in `CREATED` state. Call `server.start()` to begin accepti | `protocols` | `ProtocolRegistration[]` | `[]` | Protocol plugins (healthcheck, reflection, custom) | | `shutdown` | `ShutdownOptions` | `{}` | Graceful shutdown configuration | | `interceptors` | `Interceptor[]` | `[]` | ConnectRPC interceptors. When omitted or `[]`, no interceptors are applied. Use `createDefaultInterceptors()` from `@connectum/interceptors` for the production-ready chain. | -| `allowHTTP1` | `boolean` | `true` | Allow HTTP/1.1 connections | +| `allowHTTP1` | `boolean` | `true` | Allow HTTP/1.1 connections. Without TLS the default server is plaintext HTTP/1.1; set `false` for h2c. See the [transport matrix](/en/guide/production/transport-matrix) | | `handshakeTimeout` | `number` | `30000` | Handshake timeout in milliseconds | | `eventBus` | `EventBusLike` | `undefined` | Event bus for lifecycle management | | `http2Options` | `SecureServerOptions` | `undefined` | Additional HTTP/2 server options | +| `transportValidation` | `"error" \| "warn" \| "off"` | `"error"` | Startup validation: bidi-streaming methods on a plaintext HTTP/1.1 transport fail fast with `CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT` instead of hanging at runtime. See the [transport matrix](/en/guide/production/transport-matrix) | ### `Server` Interface