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. Socket-backed servers (net.createServer, or http.createServer with a port listen()) are not available; to serve HTTP with a node:http request listener, mount it as a custom HTTP router with nodeHttpHandler.

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.

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/json with body input keys as JSON fields. For unstructured text, send raw UTF-8 text with text/plain; for unstructured binary, send raw bytes with the appropriate MIME type. Load golem-http-params-ts for 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:http and node:https are available with comprehensive client-side support — useful when porting Node.js code or when npm packages depend on them
  • Socket-backed servers (net.createServer, or http.createServer with a port listen()) are not available; mount node:http request listeners as custom HTTP routers with nodeHttpHandler
  • node:net, node:dgram and node:dns are not supported: the worker executor refuses WASI sockets by default
  • 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 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