Making Outgoing HTTP Requests (TypeScript)

SkillAI & models

Making outgoing HTTP requests from a TypeScript Golem agent. Use when the user asks to call an external API, make HTTP requests, use fetch, or send HTTP requests from agent code.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Making Outgoing HTTP Requests (TypeScript) skill

What this skill tells your AI

The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/ts/golem-make-http-request-ts/SKILL.md and read by ahel’s review.

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", load 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 { z } from 'zod';
import { defineAgent, method, http, atomically } 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() {
      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 input key. This is true even when the endpoint has a single body parameter.

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: If the target endpoint is a Golem agent, always send application/json with parameter names as JSON keys. Load 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

Signals

GitHub stars
2k
Forks
210
Last commit
Sep 2026
Advanced
Item type
skill
Key
golem-make-http-request-ts
Source
github.com/golemcloud/golem