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. Server-side APIs (http.createServer,net.listen) are not available in WASM.
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.
There are two ways to make Golem retry the HTTP call itself when a non-2xx response 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 — no atomically(...) and no application-level
throw needed. 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 work out of the box. Idempotence mode defaults to
true, so a plainPOST /chargewith a JSON body is already eligible for status-code retry — there is no need to wrap the call inwithIdempotenceMode(true, ...). Seegolem-atomic-block-tsfor details on when (rarely) to opt out withwithIdempotenceMode(false, ...).
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// Plain fetch — no atomically(...), no withIdempotenceMode wrapper.
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).
Fallback: atomically(...)
When the retry trigger is application-level (e.g. retrying based on a parsed body field, or
combining multiple side effects into one logical step), wrap the entire request,
response-body read, and status check in atomically(...). The failed atomic block is not
committed, so recovery re-executes the fetch instead of replaying the recorded failed response.
import { atomically } from '@golemcloud/golem-ts-sdk';
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
return atomically(async () => {
const response = await fetch(url, init);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP ${response.status}: ${errorBody}`);
}
return (await response.json()) as T;
});
}Use the same pattern for third-party HTTP clients that resolve with a response and then throw from application code based on the status.
try {
const result = await fetchJson<unknown>('https://api.example.com/data');
return result;
} catch (error) {
console.error('Request failed:', error);
throw error;
}Complete Example in an Agent
import { BaseAgent, agent, atomically, endpoint } from '@golemcloud/golem-ts-sdk';
type WeatherReport = { temperature: number; description: string };
@agent({ mount: '/weather/{city}' })
class WeatherAgent extends BaseAgent {
constructor(readonly city: string) {
super();
}
@endpoint({ get: '/current' })
async getCurrent(): Promise<WeatherReport> {
return atomically(async () => {
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), the request body must match the Golem HTTP body mapping convention: non-binary body parameters are always deserialized from a JSON object where each top-level field corresponds to a method parameter name. This is true even when the endpoint has a single body parameter.
For example, given this endpoint definition:
@endpoint({ post: '/record' })
async record(body: string): Promise<void> { ... }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: If the target endpoint is a Golem agent, always send
application/jsonwith parameter names as JSON keys. See thegolem-http-params-tsskill for the full body mapping rules.
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- Server-side APIs (
http.createServer,net.listen) are not available in WASM - 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 still recorded responses. Use a
status-code-keyed retry policy (preferred) or wrap status-based throwing inatomicallywhen a retry must send a new HTTP request. - Requests are async — always use
await