Skip to Content
InvokeMaking Custom APIsHTTP Routers and FilesUsing 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.

Web Request, Response, and Headers are normalized views, not a lossless replacement for the canonical envelope. Web APIs can normalize methods and URLs, combine fields, or reject methods and statuses. The Web handler’s context exposes the original request; withRawHeaders supplies ordered byte-valued response fields.

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

See HTTP Gateway for shared storage, ingress, HTTP API and MCP ports, streaming timeouts, and resource budgets. These are platform operator settings, not application router declarations.

Troubleshooting

See Troubleshooting for routing, file serving, and OpenAPI failures.

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