Making Outgoing HTTP Requests (TypeScript)
Overview
Golem TypeScript agents run in a WebAssembly environment with a built-in fetch API. Use the standard fetch() function for all outgoing HTTP requests — it is fully supported and works with WASI HTTP under the hood.
Note: The
node:httpandnode:httpsmodules are also available with comprehensive client-side support (the client API passes the majority of Node.js compatibility tests). They can be used as an alternative, especially when porting existing Node.js code. Socket-backed servers (net.createServer, orhttp.createServerwith a portlisten()) are not available; to serve HTTP with anode:httprequest listener, mount it as a custom HTTP router withnodeHttpHandler.
GET Request
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);GET with Headers
const response = await fetch('https://api.example.com/secure', {
headers: {
'Authorization': 'Bearer my-token',
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();POST with JSON Body
const payload = { name: 'Alice', email: 'alice@example.com' };
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
const created = await response.json();PUT / DELETE
// PUT
const response = await fetch('https://api.example.com/users/123', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Updated Name' }),
});
// DELETE
await fetch('https://api.example.com/users/123', {
method: 'DELETE',
});Reading Response
const response = await fetch(url);
// Status
response.status; // e.g. 200
response.ok; // true if 2xx
response.statusText; // e.g. "OK"
// Headers
response.headers.get('Content-Type');
// Body (choose one)
const text = await response.text(); // as string
const json = await response.json(); // parsed JSON
const buffer = await response.arrayBuffer(); // raw bytesError Handling
fetch() resolves successfully when the server returns an HTTP response, even if the status is
500. From Golem’s durability point of view, this means there are two separate durable events when
you validate the response in TypeScript:
- the outgoing HTTP side effect completes and its response status/body is recorded in the oplog;
- your code later throws based on that recorded
Response.
Use a host retry policy to retry the HTTP call itself when a selected status arrives.
Preferred: a status-code-keyed retry policy
Define a named retry policy whose predicate explicitly references status-code. When the response
arrives the host transparently re-sends the request without an application-level throw.
The resolved Response is the last attempt. See the
golem-retry-policies-ts skill for the policy YAML / SDK shape.
This is the recommended path when the retry trigger is a status code (or status-code range). It also handles request-body reconstruction, idempotency checks, and replay correctly.
POST/PUT/PATCH are eligible for retry. The guest cannot opt out with an idempotence-mode switch. Make writes duplicate-safe at the peer: exactly-once processing requires the peer to enforce an idempotency key. A lost response does not prove the write failed. Do not predicate retry policies on
is-idempotent; the host no longer supplies that property.
Minimal POST example — the policy is defined once (in golem.yaml or via the SDK) and the call
site is just fetch:
# golem.yaml — under retryPolicyDefaults / <env>:
http-5xx-retry:
priority: 20
predicate:
propIn: { property: "status-code", values: [500, 502, 503, 504] }
policy:
countBox:
maxRetries: 3
inner:
exponential:
baseDelay: "200ms"
factor: 2.0// The host applies the configured status-code policy.
const response = await fetch('https://payments.example.com/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId, amount }),
});
if (!response.ok) {
// The host already retried up to maxRetries times against the 5xx policy;
// this is the *final* response after retries were exhausted.
throw new Error(`charge failed: ${response.status}`);
}
const charge = await response.json();If status retry “doesn’t seem to work”, See the golem-local-dev-server skill and look for the
HTTP status retry skipped, reason: … debug line — it pinpoints why a particular request was not
retried (e.g. BodyNotFinished, NotIdempotent, NoRetry).
Application-level retry
For a retry decision based on a parsed body, use explicit guest control flow or an SDK saga. Each new request appends a new call; earlier responses stay recorded. Throwing after a response fails the invocation and does not retry the completed request. Host transport and body-resumption retries remain enabled independently of application-level decisions.
Complete Example in an Agent
import { z } from 'zod';
import { defineAgent, method, http } from '@golemcloud/golem-ts-sdk';
const WeatherReport = z.object({ temperature: z.number(), description: z.string() });
export const WeatherAgent = defineAgent({
name: 'WeatherAgent',
id: { city: z.string() },
http: http.mount('/weather/{city}'),
methods: {
getCurrent: method({ input: {}, returns: WeatherReport, http: http.get('/current') }),
},
});
export const WeatherAgentImpl = WeatherAgent.implement({
init: ({ id }) => ({ city: id.city }),
methods: {
async getCurrent() {
const response = await fetch(
`https://api.weather.example.com/current?city=${encodeURIComponent(this.city)}`,
{ headers: { 'Accept': 'application/json' } },
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
return await response.json();
},
},
});Calling Golem Agent HTTP Endpoints
When making HTTP requests to other Golem agent endpoints (or your own), ordinary structured body inputs are deserialized from a JSON object with body input keys as top-level fields. Include only inputs not bound to path/query/header values. A single object input named payload needs {"payload": {"name": "Widget", "count": 5}}, not the bare inner object. Unstructured text and binary inputs instead consume the raw body without this wrapper.
For example, given this endpoint definition:
record: method({ input: { body: z.string() }, returns: z.void(), http: http.post('/record') }),The correct HTTP request must send a JSON object with a body field — not a raw text string:
// ✅ CORRECT — JSON object with field name matching the parameter
await fetch('http://my-app.localhost:9006/recorder/main/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: 'a' }),
});
// ❌ WRONG — raw text body does NOT match Golem's JSON body mapping
await fetch('http://my-app.localhost:9006/recorder/main/record', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: 'a',
});Rule of thumb: For structured Golem agent bodies, send
application/jsonwith body input keys as JSON fields. For unstructured text, send raw UTF-8 text withtext/plain; for unstructured binary, send raw bytes with the appropriate MIME type. Loadgolem-http-params-tsfor the restrictions and the separate CLI positional/native-literal rules; do not reuse the HTTP wrapper for CLI arguments.
Key Constraints
- Use
fetch()as the primary HTTP client — it is the standard and recommended API node:httpandnode:httpsare available with comprehensive client-side support — useful when porting Node.js code or when npm packages depend on them- Socket-backed servers (
net.createServer, orhttp.createServerwith a portlisten()) are not available; mountnode:httprequest listeners as custom HTTP routers withnodeHttpHandler node:net,node:dgramandnode:dnsare not supported: the worker executor refuses WASI sockets by default- Third-party HTTP client libraries that use
fetchornode:httpinternally (e.g.,axios) generally work; libraries that depend on native C/C++ bindings will not - All HTTP requests go through the WASI HTTP layer, which provides durable execution guarantees
- HTTP error statuses are recorded responses. Use a
status-code-keyed host retry policy; throwing after the response does not retry that completed call. - Requests are async — always use
await