Upstream Types
An upstream is the terminal destination for a request after all policies have executed. Stoma supports three upstream types, expressed as a discriminated union on the type field.
The URL and Handler upstreams work on every runtime. The Service Binding upstream is a Cloudflare Workers feature for zero-network-hop Worker-to-Worker communication.
type UpstreamConfig = | { type: "url"; target: string; rewritePath?: (path: string) => string; headers?: Record<string, string>; } | { type: "service-binding"; service: string; rewritePath?: (path: string) => string; } | { type: "handler"; handler: (c: Context) => Response | Promise<Response>; };URL Upstream
Section titled “URL Upstream”The most common upstream type. Forwards the request to an external HTTP endpoint via fetch(). Works on every runtime.
{ path: "/api/users/*", pipeline: { upstream: { type: "url", target: "https://users-service.internal", rewritePath: (path) => path.replace("/api/users", "/v2/users"), headers: { "x-forwarded-host": "gateway.example.com" }, }, },}Configuration
Section titled “Configuration”| Field | Type | Description |
|---|---|---|
type | "url" | Discriminant field. |
target | string | Target URL (e.g. "https://api.example.com"). Validated at config time. |
rewritePath | (path: string) => string | Optional. Transform the request path before forwarding. Must not change the origin. |
headers | Record<string, string> | Optional. Headers to add or override on the forwarded request. |
Resolution steps
Section titled “Resolution steps”- Build target URL. The incoming request path (and query string) is appended to the configured
targetorigin. IfrewritePathis provided, it transforms the path first. - SSRF protection. The rewritten URL is validated to ensure its origin (protocol + host + port) matches the configured
targetorigin. If the origin changes, the gateway returns a 502 error. This preventsrewritePathfunctions from redirecting traffic to unintended destinations. - Header preparation. The original request headers are cloned. Hop-by-hop headers (e.g.
connection,keep-alive,transfer-encoding,upgrade) are stripped per RFC 2616. TheHostheader is set to the upstream’s host. Any configuredheadersoverrides are applied. A W3Ctraceparentheader is added with a fresh span ID for the upstream leg. - Fetch. The request is forwarded via
fetch()withredirect: "manual"to prevent redirect-based SSRF to internal services. - Response. Hop-by-hop headers are stripped from the upstream response before returning it to the client.
Hop-by-hop headers
Section titled “Hop-by-hop headers”The following headers are stripped from both the forwarded request and the upstream response:
connectionkeep-aliveproxy-authenticateproxy-authorizationproxy-connectiontetrailertransfer-encodingupgrade
Handler Upstream
Section titled “Handler Upstream”Handler upstreams invoke a function directly — no proxying occurs. The gateway itself acts as the origin server. This is useful for health checks, static responses, development stubs, and routes that compute responses without external dependencies. Works on every runtime.
{ path: "/api/version", pipeline: { upstream: { type: "handler", handler: (c) => c.json({ version: "1.0.0", environment: "production" }), }, },}Configuration
Section titled “Configuration”| Field | Type | Description |
|---|---|---|
type | "handler" | Discriminant field. |
handler | (c: Context) => Response | Promise<Response> | Function receiving the Hono context and returning a Response. |
The handler function receives the full Hono Context, including access to PolicyContext via getGatewayContext(c), any headers set by upstream policies, and all standard Hono helpers (c.json(), c.text(), c.html(), etc.).
Example: Health check
Section titled “Example: Health check”The built-in health() function uses a handler upstream internally. You can build similar inline responses:
{ path: "/api/echo", methods: ["POST"], pipeline: { upstream: { type: "handler", handler: async (c) => { const body = await c.req.json(); return c.json({ echo: body, requestId: getGatewayContext(c)?.requestId, timestamp: new Date().toISOString(), }); }, }, },}Service Binding Upstream (Cloudflare Workers)
Section titled “Service Binding Upstream (Cloudflare Workers)”Service Binding upstreams use Cloudflare’s Worker-to-Worker communication for zero-network-hop dispatching. The bound Worker runs on the same Cloudflare infrastructure, eliminating external network latency. This upstream type is only available when deploying to Cloudflare Workers.
{ path: "/auth/*", pipeline: { upstream: { type: "service-binding", service: "AUTH_SERVICE", rewritePath: (path) => path.replace("/auth", ""), }, },}Configuration
Section titled “Configuration”| Field | Type | Description |
|---|---|---|
type | "service-binding" | Discriminant field. |
service | string | Name of the Service Binding as configured in wrangler.toml (e.g. "AUTH_SERVICE"). |
rewritePath | (path: string) => string | Optional. Transform the request path before forwarding to the bound service. |
Resolution steps
Section titled “Resolution steps”- Resolve binding. The gateway accesses
c.env[service]to retrieve the Service BindingFetcher. If the binding is not available or does not expose afetchmethod, a 502 error is returned. - Path rewrite. If
rewritePathis configured, the incoming path is transformed. The query string is preserved. - Header preparation. Hop-by-hop headers are stripped. A W3C
traceparentheader is added with a fresh span ID. - Dispatch. The request is forwarded via
binding.fetch(request). - Response. Hop-by-hop headers are stripped from the bound Worker’s response before returning it.
wrangler.toml configuration
Section titled “wrangler.toml configuration”The Service Binding must be declared in the consuming Worker’s wrangler.toml:
[[services]]binding = "AUTH_SERVICE"service = "auth-worker"environment = "production"The binding value must match the service field in the upstream config. The service value is the name of the target Worker as deployed on Cloudflare.
Config Validation
Section titled “Config Validation”Invalid upstream configuration is caught at gateway construction time:
- A
urlupstream with an unparseabletargetthrows immediately whennew URL(target)fails. - An unknown
typevalue throws aGatewayErrorwith codeconfig_error. - A
service-bindingupstream where the binding is missing from the Worker environment throws a 502 at request time (since bindings are runtime-resolved fromc.env).