Outgoing HTTP Requests from a Go Agent
Overview
Use the standard library net/http package directly. The SDK routes net/http through Golem’s wasi:http transport. For durable agents, completed request/response-body recordings replay from the oplog. Recovery can reissue a request whose response-body scope was incomplete, with the same Golem-managed idempotency key. Exactly-once external effects require the peer to deduplicate that key; recording alone does not guarantee them. No manual wiring or custom transport is needed.
Steps
- Import
net/http(andioto read the body). - Make the request —
http.Get(url), or build an*http.Requestand usehttp.DefaultClient.Do. - Read the response body, then
Closeit. - Do not add blanket manual retry loops — Golem retries eligible transient HTTP failures per the host retry policy. A panic fails the agent (see
golem-add-agent-go→ Returning Failures).
Example
package impl
import (
"io"
"net/http"
"net/url"
"myapp/agents/fetcher"
"github.com/golemcloud/golem/sdks/go/golem"
)
type state struct{}
var agent = fetcher.Agent.Implement(func(fetcher.ID) *state { return &state{} })
func init() {
agent.Handle(fetcher.Fetch, func(_ *golem.Context[state], in fetcher.FetchIn) string {
requestURL := "https://api.example.com/thing?q=" + url.QueryEscape(in.Query)
// golem.Must panics on error, which fails the agent. Use it for calls that
// should not fail normally.
resp := golem.Must(http.Get(requestURL))
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic("unexpected upstream HTTP status: " + resp.Status)
}
return string(golem.Must(io.ReadAll(resp.Body)))
})
}golem.Must(v, err) returns v or panics on err — the fail-loud idiom for errors the agent cannot handle. To surface an HTTP failure to the caller as a value instead, return a golem.Result and inspect the error yourself (see golem-add-agent-go → Returning Failures).
http.Get does not return an error for non-2xx status codes. Always check StatusCode according to the API contract. The example above treats unexpected statuses as agent failures. For intentional failures that callers should handle, declare fetcher.Fetch with output type golem.Result[string, string] and register a matching handler:
agent.Handle(fetcher.Fetch, func(_ *golem.Context[state], in fetcher.FetchIn) golem.Result[string, string] {
requestURL := "https://api.example.com/thing?q=" + url.QueryEscape(in.Query)
resp, err := http.Get(requestURL)
if err != nil {
return golem.Err[string, string]("upstream request failed")
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return golem.Err[string, string]("upstream HTTP status: " + resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return golem.Err[string, string]("upstream response read failed")
}
return golem.Ok[string, string](string(body))
})POST / custom requests
req := golem.Must(http.NewRequest("POST", url, strings.NewReader(body)))
req.Header.Set("Content-Type", "application/json")
resp := golem.Must(http.DefaultClient.Do(req))
defer resp.Body.Close()Calling Golem Agent HTTP Endpoints
When making HTTP requests to other Golem agent endpoints (or your own), ordinary structured body fields are decoded from a JSON object with the body field names as top-level keys. Include only fields not bound to path/query/header values. A single struct field named Payload needs {"payload": {"name": "Widget", "count": 5}}, not the bare inner object. Unstructured text and binary fields instead consume the raw body without this wrapper.
For example, given this endpoint definition:
type RecordIn struct{ Body string }
var Record = Agent.Method[RecordIn, golem.Unit]("record", golem.HTTP(golem.POST("/record")))The correct HTTP request sends a JSON object with a body field, not a raw text string:
// ✅ CORRECT — JSON object with the field name as the key
req := golem.Must(http.NewRequest("POST", "http://my-app.localhost:9006/recorder/main/record",
strings.NewReader(`{"body":"a"}`)))
req.Header.Set("Content-Type", "application/json")
// ❌ WRONG — a raw text body does not match Golem's JSON body mapping
req = golem.Must(http.NewRequest("POST", "http://my-app.localhost:9006/recorder/main/record",
strings.NewReader("a")))
req.Header.Set("Content-Type", "text/plain")Rule of thumb: For structured Golem agent bodies, send
application/jsonwith the body field names as JSON keys. For unstructured text, send raw UTF-8 text withtext/plain; for unstructured binary, send raw bytes with the appropriate MIME type. Loadgolem-http-params-gofor the restrictions and the separate CLI positional/Go-literal rules; do not reuse the HTTP wrapper for CLI arguments.
Durability & retries
- In durable mode, completed recordings replay without contacting the external service. An interrupted response-body scope can reissue the recorded request with the same idempotency key. For mutating requests, require peer-side deduplication before relying on exactly-once effects. Ephemeral invocations do not recover by replay.
- Do not layer blanket retry/backoff loops over host retries. Only eligible failures and statuses are retried, within the configured policy; permanent failures can still reach your code. Customize with the retry-policy helpers when the strategy must change, and handle final failures explicitly.
Key Constraints
- Target is WASM: there are no raw sockets.
net.Dial, most database drivers, and custom transports won’t work — outgoing traffic goes through the SDK’s WASI-backednet/httptransport. - Async handles cannot outlive an invocation: read/consume the response within the same handler; don’t stash an unresolved response/body in agent state to use later.
Related Guides
| Guide | Description |
|---|---|
golem-add-http-endpoint-go | Expose this agent over HTTP (incoming requests) |
golem-call-another-agent-go | Call another Golem agent instead of an external HTTP service |
golem-add-agent-go | Define the agent making the request |