Skip to Content
InvokeUsing HTTP Routers and Files

Using HTTP routers and files

Use an HTTP router to handle streaming requests, serve static assets, or integrate an HTTP framework. To publish files created by a durable agent, add live file mappings to that agent instead. The router and file semantics page explains how these features interact with code-first endpoints, authentication, and durable execution.

The examples below belong in an existing Golem application. Use a CLI and SDK from the same Golem version. Commands assume a local server on your machine, with the application gateway on port 9006.

Handling streaming requests

Define a router with a unique type name and a literal mount path. This EchoRouter streams an upload back to the caller at /echo. It does not collect the upload into memory or calculate its length first.

import { defineHttpRouter } from '@golemcloud/golem-ts-sdk'; export const EchoRouter = defineHttpRouter('EchoRouter') .mount('/echo') .implement((request) => new Response(request.body));

The Web adapter accepts standard Request and Response objects, so a framework with a Fetch-compatible handler can supply the function passed to .implement. Keep its body streaming. For the original, unnormalized HTTP envelope, use .implementRaw instead; see Working with headers.

Merge this deployment into your application’s manifest:

golem.yaml
httpApi: deployments: local: - domain: localhost:9006 scheme: http agents: EchoRouter: {}

Build, deploy, and send an upload:

golem build -L golem deploy -L -Y printf 'stream me' | curl --data-binary @- http://localhost:9006/echo

The response is stream me. For a router with several routes, dispatch on the request’s method and path, and return 404 for unknown routes. The canonical request uses the full public path; framework adapters may present mount-relative paths as noted in their tabs. Routing precedence explains when a code-first endpoint or file mapping takes priority over the handler.

Working with headers

Keep repeated headers separate, especially Set-Cookie. The raw APIs use lowercase names and byte-valued fields. For example, add two cookies to a streaming response:

The Web adapter already preserves separate Set-Cookie fields. For byte-valued or strictly ordered fields, import withRawHeaders from @golemcloud/golem-ts-sdk and replace the handler with:

.implement((request) => withRawHeaders(new Response(request.body), [ { name: 'set-cookie', value: new TextEncoder().encode('first=one') }, { name: 'set-cookie', value: new TextEncoder().encode('second=two') }, ]));

withRawHeaders replaces all response headers with this list, rather than adding to Response.headers. Include every header you want emitted.

For canonical methods, paths, queries, and headers without Web API normalization, replace .implement(...) with a raw handler:

.implementRaw((request) => ({ status: 200, headers: [ { name: 'set-cookie', value: new TextEncoder().encode('first=one') }, { name: 'set-cookie', value: new TextEncoder().encode('second=two') }, ], body: request.body, }));

A Web handler can also read context.rawRequest from its second argument to inspect the original head. The body still has only one reader.

Let Golem handle transfer framing. Omit Content-Length unless you know the exact emitted byte count. Release unused input and stop producing when the output consumer closes. Do not turn stream errors into normal EOF. See the streaming contract for bodyless responses and the distinction between EOF and invocation success.

Serving static files

Create a router with file mappings and no handler. This StaticRouter serves an explicit index at /static and assets below /static/assets/:

import { defineHttpRouter } from '@golemcloud/golem-ts-sdk'; export const StaticRouter = defineHttpRouter('StaticRouter') .mount('/static') .static('/', '/public/index.html') .static('/assets/*', '/public/assets/$1') .implement();

Create index.html and message.txt beside golem.yaml. Provision them as read-only files under the router’s type name, and add it to the HTTP deployment:

golem.yaml
agents: StaticRouter: files: - sourcePath: ./index.html targetPath: /public/index.html permissions: read-only - sourcePath: ./message.txt targetPath: /public/assets/message.txt permissions: read-only httpApi: deployments: local: - domain: localhost:9006 scheme: http agents: EchoRouter: {} StaticRouter: {}

Redeploy and check the index, metadata, and a byte range:

golem deploy -L -Y curl http://localhost:9006/static curl -I http://localhost:9006/static/assets/message.txt curl -i -H 'Range: bytes=0-3' http://localhost:9006/static/assets/message.txt

Use a nonempty message.txt for the range example. Static responses include a strong ETag and Cache-Control: no-cache. Send the returned ETag in If-None-Match to revalidate; an unchanged asset returns 304. See the file HTTP reference for HEAD, preconditions, and range edge cases.

You can add the same mappings to a router with a handler: matching GET/HEAD files are served first, and an absent file falls through to the handler. For an override directory, declare two /assets/* mappings, first to /overrides/$1, then to /public/assets/$1. Order matters; see ordered mappings.

Expose only directories intended for HTTP access. There is no implicit index lookup, directory listing, or symlink support. Keep secrets outside exposed roots and protect private mounts with authentication.

Serving live agent files

Use a normal durable agent when files are created or modified at runtime. Bind its constructor parameters in the mount and expose only the desired files. This FileOwner creates /value.txt during initialization and overwrites it on update:

import { defineAgent, http, method } from '@golemcloud/golem-ts-sdk'; import * as fs from 'node:fs'; import { z } from 'zod'; export const FileOwner = defineAgent({ name: 'FileOwner', id: { name: z.string() }, http: http.mount('/files/{name}', { exposeFiles: [{ route: '/value', path: '/value.txt' }], }), methods: { update: method({ input: {}, returns: z.string() }) }, }); export const FileOwnerImpl = FileOwner.implement({ init({ id }) { fs.writeFileSync('/value.txt', `initial:${id.name}`); return {}; }, methods: { update() { fs.writeFileSync('/value.txt', 'updated'); return 'updated'; }, }, });

Add FileOwner: {} beside the routers in the deployment’s agents map, then:

golem deploy -L -Y curl http://localhost:9006/files/alice/value golem agent invoke -L 'FileOwner("alice")' update curl http://localhost:9006/files/alice/value

The first read returns initial:alice; the second returns updated. Reads use Cache-Control: no-store with no ETag. No exported method is called to serve the file, but the first read can initialize the agent. A slow download can delay other work on that agent; see live-file lifecycle.

Calling an agent from a router

Call ordinary agents through the same clients used for agent-to-agent communication. For example, inside an async handler, update the file owner before constructing the response:

await FileOwner.client.get({ name: 'from-router' }).update();

For cross-component calls, declare the dependency and generate its client as described in Calling an agent in another component. A router’s cancellation does not roll back completed agent calls. Client retries are new requests and can repeat effects; see cancellation and recovery.

Publishing OpenAPI

Add an OpenAPI provider to describe your router’s operations. For EchoRouter, describe POST / relative to its mount; Golem publishes it as POST /echo. The provider must return OpenAPI 3.1.0, not an OpenAPI 3.0 or YAML document.

Insert this before .implement(...); the SDK serializes the returned object:

.openApi(() => ({ openapi: '3.1.0', info: { title: 'Echo API', version: '1' }, paths: { '/': { post: { operationId: 'echo', responses: { '200': { description: 'Streamed request bytes' } }, }, }, }, }))

Keep providers finite and deterministic. Use unique component names and operation IDs across the domain, and local references that resolve within the provider’s own document. See OpenAPI generation for the supported subset, security merging, conflicts, caching, and size limits.

After redeployment, retrieve both formats:

golem deploy -L -Y curl http://localhost:9006/openapi.json curl http://localhost:9006/openapi.yaml

To place them under /docs, add openapiEndpoint: /docs beside domain and scheme in the HTTP deployment. The URLs become /docs/openapi.json and /docs/openapi.yaml. Set scheme to match the public origin. Built-in local environments default to http, cloud environments to https; custom-server environments must set it explicitly.

OpenAPI endpoints are public even when a router requires authentication. Do not include secret values, private examples, or requester-specific information. Apply a separate ingress policy if the description must be private.

Protecting mounts

Configure authentication and CORS on the router mount, not its handler method. Use the same security scheme configuration as code-first endpoints. The router declarations accept these options:

.mount('/echo', { auth: true, cors: ['https://app.example.com'] })

Protect each child mount explicitly: it does not inherit authentication from a parent. Test denial before checking file existence, and test browser preflight separately from plain OPTIONS. See authentication and failures for host behavior and status codes.

Configuring self-hosted deployments

These settings are for platform operators. Applications use worker-service’s apps gateway (custom_request_port, default 9006), not its management REST or gRPC port.

Configuring shared storage

Static files are read directly by worker-service. Configure registry, executors, and every serving worker-service to share the initial-file blob store and namespace. For a local/shared-volume topology:

[blob_storage] type = "LocalFileSystem" [blob_storage.config] root = "/blob_storage"

Mount the same volume into all services; identical paths in isolated containers are not shared storage. The equivalent environment variables are GOLEM__BLOB_STORAGE__TYPE=LocalFileSystem and GOLEM__BLOB_STORAGE__CONFIG__ROOT=/blob_storage. The PostgreSQL compose example  shows the shared volume wiring.

For S3, configure worker-service with the same initial_agent_files_bucket, object_prefix, region, endpoint, and path-style settings as the other services. Use the complete service configuration for the other required buckets. Grant worker-service read access, including ranged reads, and supply credentials through your deployment’s secret mechanism. InMemory storage is not shared across processes; worker-service does not support KVStoreSqlite.

Configuring ingress

Use an ingress with an HTTP/2 cleartext upstream (h2c prior knowledge), preserve per-stream resets, and disable request/response buffering for streaming routes. The gateway also accepts HTTP/1, but does not support Upgrade: h2c.

To accept public-origin headers from a TLS-terminating proxy, list its direct peer IP in worker-service configuration:

[route_resolver] trusted_ingress_addresses = ["10.0.0.10"]

The default list is empty. Configure that proxy to remove client forwarding fields and write exactly one valid X-Forwarded-Proto and one X-Forwarded-Host. Both are required; comma-separated proxy chains and Forwarded are not alternative origin sources. Keep the listener private to the intended ingress.

Test disconnect/reset through the actual proxy, including while waiting for a response head. A client timeout alone does not prove upstream cancellation. Keep HTTP/2 enabled: some pending live-file requests over HTTP/1 detect disconnect only at the exchange deadline.

Setting timeouts and resource budgets

Tune worker-service’s session settings to your workload. The defaults are:

[http_session] exchange_timeout = "5m" retry_deadline = "10s" retry_delay = "100ms" max_resume_attempts = 4 cleanup_timeout = "2s" retained_input_bytes = 4194304 retained_input_frames = 32 event_queue_frames = 2 command_queue_frames = 16 max_event_bytes = 17825792

Use exchange_timeout to bound the whole handler or live-file exchange, including initialization, queueing, and streaming. There is no separate executor file-read deadline. Expiry returns 504 before commitment or aborts an already-started response. Recovery uses the same surviving invocation, with four reattachments allowed over the exchange’s lifetime; it cannot replace a lost executor invocation.

Use ingress concurrency limits to control live-file load: there is no dedicated per-agent read queue cap or executor-wide outstanding-read limit. Slow readers can delay an agent’s other work. durable_streams.load does not configure these reads.

The byte/frame values bound encoded session buffering, not total upload size. Input encoding includes schema and frame overhead. Queued output is bounded by event_queue_frames × max_event_bytes, not all per-request allocations.

OpenAPI generation uses separate limits and is not bounded by exchange_timeout. Route lookup caching (route_resolver.router_cache_ttl = "10m", capacity 1024, eviction period 1 minute) is also separate from the OpenAPI document cache.

Troubleshooting

  • Unexpected handler or 404: check concrete code-first routes, the selected mount, and declaration order. Only absent files fall through; permission, initialization, and storage failures do not.
  • Static file returns 500: confirm it is provisioned read-only and that worker-service can read the indexed blob from shared storage.
  • Live read stalls: check agent initialization and preceding work, then test cancellation through the ingress and review the exchange deadline.
  • OpenAPI returns 502: check provider JSON, size limits, local references, and conflicting paths/components/operation IDs. A provider failure does not disable ordinary router traffic.

Use status codes, safe error categories, and router/deployment identities in diagnostics. Do not log HTTP bodies, provider documents, secrets, or internal paths.

Complete examples are available in the repository for TypeScript , Rust , Scala , MoonBit , and Effect .