HTTP Transport
In addition to stdin/stdout, vgi-rpc can serve methods over HTTP using the createHttpHandler() function. This enables browser clients, load balancing, and standard HTTP infrastructure.
Quick start
Section titled “Quick start”import { Protocol, float, createHttpHandler } from "@query-farm/vgi-rpc";
const protocol = new Protocol("Calculator");
protocol.unary("add", { params: { a: float, b: float }, result: { result: float }, handler: async ({ a, b }) => ({ result: a + b }),});
const handler = createHttpHandler(protocol);Bun.serve({ port: 8080, fetch: handler });Compatibility
Section titled “Compatibility”createHttpHandler(protocol, options) returns a standard (Request) => Response | Promise<Response> function compatible with:
- Bun —
Bun.serve({ fetch: handler }) - Deno —
Deno.serve(handler) - Cloudflare Workers —
export default { fetch: handler }
Routes
Section titled “Routes”All routes are served under a configurable prefix. The default prefix is "" (root), so the examples below show the root-mounted paths. When you set a prefix (e.g. /api), it is prepended to every path.
| Route | Method | Description |
|---|---|---|
/{method} | POST | Unary method call |
/{method}/init | POST | Initialize a stream (producer or exchange) |
/{method}/exchange | POST | Send/receive a stream batch |
/__describe__ | POST | Machine-readable Arrow describe batch |
/__upload_url__/init | POST | Request a pre-signed upload URL (when uploadUrlProvider is set) |
/__session__ | DELETE | Tear down a sticky session (when enableSticky is set) |
/describe | GET | HTML describe / API-reference page |
/ | GET | HTML landing page |
/health | GET | JSON health probe (auth-exempt) |
POST requests carrying Arrow bodies must use Content-Type: application/vnd.apache.arrow.stream (exported as ARROW_CONTENT_TYPE).
When OAuth is configured (see OAuth), additional routes are mounted: GET /_oauth/callback, GET /_oauth/logout, POST /_oauth/token, and GET /.well-known/oauth-protected-resource{prefix}.
Stateless streaming
Section titled “Stateless streaming”HTTP streaming uses sealed state tokens to carry stream state across requests, so no server-side session storage is required:
- Client calls
/{method}/initwith parameters → receives the first batch plus a state token - Client calls
/{method}/exchangewith the state token + input batch → receives an output batch plus a fresh token - Repeat step 2 until the stream ends (the final response carries no token)
State tokens are sealed with XChaCha20-Poly1305 AEAD (wire format v4):
- The Poly1305 authentication tag makes tokens tamper- and forgery-resistant — any modification to the ciphertext fails to decrypt.
- Tokens are bound to the authenticated principal via the AEAD associated data (AAD). A token sealed for principal A cannot be replayed by principal B (or by an anonymous caller, and vice versa).
- Tokens are time-limited (default
tokenTtl: 1 hour;0disables the TTL check). - The sealing key is the 32-byte
tokenKeymaster key. If you omit it, a random key is generated at startup — fine for a single process, but tokens then won’t survive a restart or work across load-balanced workers. Supply a sharedtokenKeyto make tokens portable across workers.
Configuration
Section titled “Configuration”Pass HttpHandlerOptions to customize behavior:
const handler = createHttpHandler(protocol, { prefix: "/api", // path prefix for all routes (default: "" / root) tokenKey: myKey, // 32-byte XChaCha20-Poly1305 master key (random if omitted) tokenTtl: 7200, // state-token TTL in seconds (default: 3600; 0 disables) serverId: "my-server", // server ID in response metadata (random if omitted) corsOrigins: "*", // CORS allowed origins corsMaxAge: 7200, // Access-Control-Max-Age for preflight (default: 7200; null omits) maxRequestBytes: 10_000_000, // max request body size maxDecompressedRequestBytes: 160_000_000, // cap on decompressed request size (zstd-bomb defense) maxResponseBytes: 5_000_000, // response body cap (hard for unary/exchange, soft for producer) maxExternalizedResponseBytes: 50_000_000, // cap on externalized payload bytes per response compressionLevel: 3, // enable response compression (gzip/zstd) at this level authenticate: myAuthFn, // authentication callback (see Authentication) enableSticky: true, // opt-in sticky sessions});Key options:
| Option | Description |
|---|---|
prefix | Path prefix for all routes. Default "" (root). |
tokenKey | 32-byte XChaCha20-Poly1305 master key sealing stream-state tokens. Random if omitted. |
tokenTtl | State-token TTL in seconds. Default 3600; 0 disables TTL checks. |
corsOrigins | CORS allowed origins; enables CORS headers when set. |
corsMaxAge | Access-Control-Max-Age for preflight responses. Default 7200; null omits it. |
maxRequestBytes | Max request body size. Advertised via VGI-Max-Request-Bytes. |
maxDecompressedRequestBytes | Cap on post-decompression request size (zstd/gzip bomb defense). Defaults to maxRequestBytes * 16 when that is set. |
maxResponseBytes | Response body cap. Hard for unary/exchange, soft for producer (overshoot mints a continuation token). |
maxExternalizedResponseBytes | Hard cap on bytes uploaded to external storage during one response. |
compressionLevel | Enables response compression (gzip/zstd) at this level. |
authenticate | Per-request authentication callback. |
enableSticky | Opt-in sticky sessions. See Sticky Sessions. |
externalLocation / uploadUrlProvider / maxUploadBytes | Large-payload externalization. See Large Payloads. |
dispatchHook / onServeStart | Observability / lifecycle hooks. |
serverId | Server ID in response metadata. Random if omitted. |
maxStreamResponseBytes | Deprecated — use maxResponseBytes. The producer-only soft cap; kept for backward compatibility. |
See Configuration for full details on each option.
Request and response compression
Section titled “Request and response compression”The handler transparently decompresses request bodies sent with Content-Encoding: zstd or gzip. maxDecompressedRequestBytes caps the decompressed size to defend against decompression bombs (a tiny compressed frame inflating to hundreds of MB).
When compressionLevel is set, responses are compressed (zstd preferred when the runtime can encode it, otherwise gzip) according to the client’s Accept-Encoding. The server advertises what it can produce via VGI-Supported-Encodings.
See the Compression guide for details.
Set corsOrigins to enable CORS headers on responses:
const handler = createHttpHandler(protocol, { corsOrigins: "*",});The handler answers OPTIONS preflight requests automatically. The preflight reflects the request’s Access-Control-Request-Headers back in Access-Control-Allow-Headers, so clients can send custom VGI request headers without a hard-coded allow-list. corsMaxAge controls how long browsers cache the preflight (default 2 hours; null omits the header), and the relevant VGI response headers are surfaced via Access-Control-Expose-Headers.
Health endpoint
Section titled “Health endpoint”GET /health returns a small JSON document ({ "status": "ok", ... }) and is exempt from authentication, so orchestrators and load balancers can probe the server even when every RPC endpoint requires auth. Disable it with enableHealthEndpoint: false.
Cookies for unary methods
Section titled “Cookies for unary methods”Unary handlers can read request cookies — the handler parses the incoming Cookie header per request and exposes the values to the handler context. OAuth uses a _vgi_auth cookie to carry the authenticated session for browser flows.
Sticky sessions
Section titled “Sticky sessions”Sticky sessions are opt-in via enableSticky (with stickyDefaultTtl and stickyEchoHeaders). When enabled, the server advertises VGI-Sticky-Enabled: true, honors session headers, and exposes a DELETE /__session__ teardown endpoint. See Sticky Sessions for the full lifecycle.
Large payloads / external storage
Section titled “Large payloads / external storage”Large response (and request) payloads can be externalized to object storage instead of riding inline on the wire, leaving only small pointer batches. Configure externalLocation, uploadUrlProvider, and maxUploadBytes. See Large Payloads for details.
Authentication
Section titled “Authentication”Pass an authenticate callback to protect your endpoints. vgi-rpc ships with factories for bearer tokens, JWT, mTLS, and XFCC. See the Authentication guide and the OAuth guide for full details.
Quick example with bearer tokens:
import { createHttpHandler, bearerAuthenticateStatic, AuthContext } from "@query-farm/vgi-rpc";
const auth = bearerAuthenticateStatic({ tokens: { "key-abc123": new AuthContext("apikey", true, "alice"), },});
const handler = createHttpHandler(protocol, { tokenKey: myKey, authenticate: auth,});Multiple strategies can be combined with chainAuthenticate — for example, accepting both mTLS and bearer tokens:
import { chainAuthenticate, mtlsAuthenticateSubject, bearerAuthenticateStatic, AuthContext } from "@query-farm/vgi-rpc";
const auth = chainAuthenticate( mtlsAuthenticateSubject({ allowedSubjects: new Set(["my-service"]) }), bearerAuthenticateStatic({ tokens: { "sk-ci": new AuthContext("apikey", true, "ci-bot") } }),);Testing with the CLI
Section titled “Testing with the CLI”The Python CLI supports HTTP transport:
# Describevgi-rpc --url http://localhost:8080 describe
# Call a unary methodvgi-rpc --url http://localhost:8080 call add a=1.0 b=2.0