HTTP Routers in Go
Overview
An HTTP router handles raw requests under a mount, the way an HTTP server does. Unlike an agent’s HTTP endpoints (see golem-add-http-endpoint-go), where the platform maps a request onto a method call, a router receives the request exactly as the client sent it and streams back its own response.
A router is its own kind of agent: it has no constructor parameters, a fresh instance serves each request, and it takes no snapshots. It cannot be called like an ordinary agent, but it can call ordinary agents.
Mounting a net/http handler
Any http.Handler works — http.ServeMux, a third-party router, or an existing service:
package routers
import (
"io"
"net/http"
"github.com/golemcloud/golem/sdks/go/golem"
)
var Site = golem.DefineHTTPRouter(golem.RouterSpec{
Name: "Website",
Mount: "/web", // literal segments only
Auth: false, // auth and CORS belong to the mount
CORS: []string{"*"},
})
func init() {
mux := http.NewServeMux()
mux.HandleFunc("POST /web/echo", func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, r.Body) // streams in both directions
})
Site.Handle(mux)
}Blank-import the package from the component’s main.go, and add the router to an httpApi deployment in golem.yaml (see golem-configure-api-domain):
httpApi:
deployments:
local:
- domain: localhost:9006
scheme: http
agents:
Website: {}What the handler sees
r.URL.Pathis the full public path, mount included (/web/echo). To route relative to the mount, wrap the mux:Site.Handle(http.StripPrefix("/web", mux)).r.URL.RawPath,r.URL.RawQueryandr.RequestURIkeep the original escapes;r.Hostis the authority;r.TLSis non-nil for https.r.Bodystreams the request body; it is read at most once.
Writing the response
- The status and headers are sent on the first
Write,WriteHeaderorFlush, or when the handler returns. Header changes after that are ignored, as innet/http. - The handler may keep writing after the head is sent; each
Writestreams out with backpressure. - Repeated headers stay separate (
w.Header().Add("Set-Cookie", …)twice). Names are sent lowercase. - A missing
Content-Typeis sniffed from the first bytes;204,304and HEAD responses carry no body. - A panic before the head is sent traps the component, so the invocation fails, as in Rust. A panic after it fails the body stream, so the client sees a broken response rather than a short one that looks complete.
- The host owns framing: never set
Transfer-Encoding.
The raw envelope
When net/http’s view loses something you need — exact header bytes, header order, or an absent vs empty query — use HandleRaw:
Site.HandleRaw(func(ctx context.Context, req golem.HTTPRequest) golem.HTTPResponse {
// req.Method, req.Path (full, still escaped), req.Query (golem.Option[string]),
// req.Headers ([]golem.HTTPHeader{Name, Value []byte}), req.Body (golem.AgentStream[[]byte])
return golem.HTTPResponse{Status: 200, Headers: nil, Body: req.Body}
})A router has one handler: either Handle or HandleRaw.
Configuration
Declare a config type with DefineConfiguredHTTPRouter and read it from the request’s context:
type SiteConfig struct {
Greeting string
APIKey golem.Secret[string]
}
var Site = golem.DefineConfiguredHTTPRouter[SiteConfig](golem.RouterSpec{Name: "Website", Mount: "/web"})
mux.HandleFunc("GET /web/hello", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, Site.Config(r.Context()).Greeting)
})In a raw handler or OpenAPI provider, pass its ctx. Calling Config with any other context panics. Config values are set in golem.yaml under agents: <RouterName>: config: (see golem-add-config-go).
The request’s principal
Site.Principal(r.Context()) (or the raw handler’s ctx) is the principal of the request: the authenticated user behind a mount with Auth: true, otherwise a golem.AnonymousPrincipal. The OpenAPI provider serves no request, so there it is nil.
mux.HandleFunc("GET /web/me", func(w http.ResponseWriter, r *http.Request) {
if p, ok := Site.Principal(r.Context()).(golem.OidcPrincipal); ok {
io.WriteString(w, p.Sub)
return
}
http.Error(w, "login required", http.StatusUnauthorized)
})Static files
StaticFiles serves files of the component — provisioned with the manifest’s files (see golem-add-initial-files) — before the handler is consulted:
var Site = golem.DefineHTTPRouter(golem.RouterSpec{
Name: "Website",
Mount: "/web",
StaticFiles: []golem.FileMapping{
{Route: "/", Path: "/site/index.html"},
{Route: "/assets/*", Path: "/site/assets/$1"},
},
})A matching GET or HEAD is answered from the file; a missing file falls through to the handler. A router may have static files and no handler at all. FileResponseHeaders adds response headers, in order, to every static file served, e.g. []golem.FileResponseHeader{{Name: "content-security-policy", Value: "default-src 'self'"}}; headers the platform manages itself (content-type, cache-control, the CORS headers, …) are rejected at deploy. To serve files an ordinary agent writes at runtime, use Mount.ExposeFiles instead (see golem-add-http-endpoint-go).
OpenAPI
Register a provider returning an OpenAPI 3.1.0 JSON document whose paths are relative to the mount; the host places it under the mount and merges it into the deployment’s openapi.json:
Site.OpenAPI(func(ctx context.Context) string {
return `{"openapi":"3.1.0","info":{"title":"Site","version":"1"},"paths":{"/echo":{"post":{"responses":{"200":{"description":"echo"}}}}}}`
})Key Constraints
Mounthas literal segments only — a router has no identity to capture in the path.- One handler per router (
HandleorHandleRaw) and at most one OpenAPI provider. - Auth and CORS are set on the
RouterSpec, not per route. - Code-first endpoints of ordinary agents and file mappings take precedence over a router’s handler for the paths they match.
Related Guides
| Guide | Description |
|---|---|
golem-add-http-endpoint-go | Map requests onto ordinary agent methods instead; expose an agent’s live files |
golem-configure-api-domain | Deploy routers under an httpApi domain |
golem-add-config-go | Declare and set config values |
golem-call-another-agent-go | Call ordinary agents from a router |