Skip to Content
How-To GuidesGoHTTP Routers in Go

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.Path is 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.RawQuery and r.RequestURI keep the original escapes; r.Host is the authority; r.TLS is non-nil for https.
  • r.Body streams the request body; it is read at most once.

Writing the response

  • The status and headers are sent on the first Write, WriteHeader or Flush, or when the handler returns. Header changes after that are ignored, as in net/http.
  • The handler may keep writing after the head is sent; each Write streams out with backpressure.
  • Repeated headers stay separate (w.Header().Add("Set-Cookie", …) twice). Names are sent lowercase.
  • A missing Content-Type is sniffed from the first bytes; 204, 304 and 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

  • Mount has literal segments only — a router has no identity to capture in the path.
  • One handler per router (Handle or HandleRaw) 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.
GuideDescription
golem-add-http-endpoint-goMap requests onto ordinary agent methods instead; expose an agent’s live files
golem-configure-api-domainDeploy routers under an httpApi domain
golem-add-config-goDeclare and set config values
golem-call-another-agent-goCall ordinary agents from a router