# Golem Make HTTP Request TS

> 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.

- Skill: `golemcloud/golem-make-http-request-ts` (Agent Skill)
- Install (CLI): `npx skillmds@latest add golemcloud/golem-make-http-request-ts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/golemcloud/golem-make-http-request-ts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: golemcloud (https://skillmd.com/u/golemcloud)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/golemcloud/golem-make-http-request-ts

---


# 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

```typescript
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
```

## GET with Headers

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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`:

```yaml
# 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
```

```typescript
// 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.

```typescript
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.

```typescript
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

```typescript
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:

```typescript
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:

```typescript
// ✅ 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`

