Skip to Content
How-To GuidesTypeScriptMaking Outgoing HTTP Requests (TypeScript)

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:http and node:https modules 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 bytes

Error 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:

  1. the outgoing HTTP side effect completes and its response status/body is recorded in the oplog;
  2. 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 plain POST /charge with a JSON body is already eligible for status-code retry — there is no need to wrap the call in withIdempotenceMode(true, ...). See golem-atomic-block-ts for details on when (rarely) to opt out with withIdempotenceMode(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/json with parameter names as JSON keys. See the golem-http-params-ts skill for the full body mapping rules.

Key Constraints

  • Use fetch() as the primary HTTP client — it is the standard and recommended API
  • node:http and node:https are 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 fetch or node:http internally (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 in atomically when a retry must send a new HTTP request.
  • Requests are async — always use await
Last updated on