> ## Documentation Index
> Fetch the complete documentation index at: https://growthx-changeset-release-main.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# @outputai/http

> Make traceable HTTP requests from your steps

The `@outputai/http` package provides Fetch and Ky clients instrumented for [tracing](/operations/tracing). Every request — URL, method, status code, and timing — is recorded as a child node in the trace tree.

It exposes two entry points:

* `outputFetch` — a Fetch-compatible function backed by [Undici](https://undici.nodejs.org/).
* `createKyClient` — creates a [Ky](https://github.com/sindresorhus/ky) client that uses `outputFetch`.

Ky and Undici are peer dependencies. Current npm and pnpm versions install compatible peers automatically, so the normal package installation is enough:

```bash theme={null}
pnpm add @outputai/http
```

Install Ky or Undici explicitly only when you need to pin their versions or resolve a peer-version conflict.

## Using Instrumented Fetch

Use `outputFetch` when you want the standard Fetch API:

```typescript theme={null}
import { outputFetch } from '@outputai/http';

const response = await outputFetch( 'https://api.example.com/users', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify( { name: 'Ada' } )
} );

if ( !response.ok ) {
  throw new Error( `Request failed with status ${response.status}` );
}

const user = await response.json();
```

Like native Fetch, `outputFetch` returns responses for HTTP error statuses. It only throws for failures such as DNS errors, timeouts, and aborts.

`outputFetch` accepts URL strings, `URL` objects, and both Node and Undici `Request` objects. Node `Request` and `FormData` inputs are normalized into the Undici realm before the request is sent. Keep `Request` and `FormData` from the same family when using both in one call.

## Creating a Ky Client

The typical pattern is to create a client per external service in your [clients directory](/clients), then import it in your steps:

```typescript clients/jina.ts theme={null}
import { createKyClient } from '@outputai/http';

export const client = createKyClient({
  prefix: 'https://r.jina.ai',
  headers: {
    'Authorization': `Bearer ${process.env.JINA_API_KEY}`,
    'Accept': 'application/json'
  },
  timeout: 30000
});
```

`createKyClient` accepts Ky's complete options object and returns a standard Ky instance. All Ky methods, hooks, retries, timeout behavior, and `.extend()` support remain available.

By default, the client uses `outputFetch`. Providing Ky's `fetch` option overrides it, so requests made by that client are not automatically traced by Output.

You can extend clients to create specialized instances:

```typescript theme={null}
const authenticatedClient = client.extend({
  headers: { 'X-Custom-Header': 'value' }
});
```

## HTTP Methods

```typescript theme={null}
// GET
const response = await client.get('https://acme.com');
const data = await response.json();

// POST
const response = await client.post('companies', {
  json: { url: 'https://acme.com' }
});
const result = await response.json();

// PUT
await client.put('companies/123', {
  json: { industry: 'SaaS' }
});

// DELETE
await client.delete('companies/123');
```

## Response Bodies

Both APIs follow normal Fetch semantics: callers own the returned response body. Always consume the body with
`.json()`, `.text()`, `.arrayBuffer()`, or another body reader when you need the payload.

If you only need response metadata from a non-`HEAD` request, such as `response.url`, `response.status`, or headers, cancel
the unused body so the underlying connection and native buffers can be released:

```typescript theme={null}
const response = await client.get('https://example.com', {
  redirect: 'follow'
});

try {
  return response.url;
} finally {
  await response.body?.cancel();
}
```

## Using in Steps

Wrap HTTP calls in steps for automatic retry and tracing:

```typescript steps.ts theme={null}
import { step } from '@outputai/core';
import { jina } from '../../clients/jina.js';
import { ScrapePageInput, ScrapePageOutput } from './types.js';

export const scrapePage = step({
  name: 'scrapePage',
  description: 'Scrape a web page using Jina Reader API',
  inputSchema: ScrapePageInput,
  outputSchema: ScrapePageOutput,
  fn: async (url) => {
    const response = await jina.get(url);
    const data = await response.json();

    return {
      title: data.data?.title ?? '',
      content: data.data?.content ?? '',
      url: data.data?.url ?? url
    };
  }
});

// types.ts
// import { z } from '@outputai/core';
//
// export const ScrapePageInput = z.string();
//
// export const ScrapePageOutput = z.object({
//   title: z.string(),
//   content: z.string(),
//   url: z.string()
// });
```

## Tracing

All requests made with `@outputai/http` are automatically traced — no configuration needed. In your trace files, HTTP calls appear as children of the step that made them:

```json theme={null}
{
  "kind": "http",
  "name": "request",
  "input": { "method": "GET", "url": "https://r.jina.ai/https://acme.com" },
  "output": { "status": 200, "statusText": "OK" }
}
```

See [Tracing](/operations/tracing) for details on the trace format.

## Request Events

Every call made through `outputFetch` or a client returned by `createKyClient` emits an **`http:request`** hook event — independent of whether you attach a cost. Subscribe to it with [`on`](/packages/core#hooks) for logging, alerting, or per-vendor metrics:

```typescript theme={null}
import { on } from '@outputai/core/hooks';
import type { HttpRequestEvent } from '@outputai/http';

on<HttpRequestEvent>( 'http:request', async ( { eventId, workflowDetails, payload } ) => {
  if (!workflowDetails || !payload) {
    return;
  }

  console.log( `[${payload.outcome}] ${payload.method} ${payload.url} ${payload.status ?? '-'} ${payload.durationMs}ms`, {
    eventId,
    workflowId: workflowDetails.workflowId
  } );
} );
```

The handler receives an event envelope. Request-specific fields are available under `payload`:

| Field                 | Description                                                                                                                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`             | UUID v4 stamped per emit. Stable per-emit idempotency key — `http:request` and `cost:http:request` for the same fetch get distinct `eventId`s, so dedup keyed on `eventId` won't collapse the two. |
| `eventDate`           | Millisecond epoch timestamp for when the event was emitted.                                                                                                                                        |
| `activityInfo?`       | Temporal [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) when the request is made in an activity context.                                                           |
| `workflowDetails?`    | Output's serializable subset of Temporal [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo), when available.                                           |
| `outputActivityKind?` | Output activity kind when available. Possible values are `step`, `evaluator`, and `internal_step`.                                                                                                 |
| `payload.requestId`   | UUID generated per request inside `@outputai/http`. Shared between the matching `http:request` and `cost:http:request` events for cross-event correlation.                                         |
| `payload.method`      | HTTP method (uppercase).                                                                                                                                                                           |
| `payload.url`         | Absolute request URL.                                                                                                                                                                              |
| `payload.status`      | HTTP status code; `undefined` on network failure.                                                                                                                                                  |
| `payload.durationMs`  | Elapsed time from request issuance to response (or failure), in milliseconds.                                                                                                                      |
| `payload.outcome`     | One of `'success'` (2xx-3xx), `'error'` (status >= 400), `'failure'` (DNS / timeout / abort).                                                                                                      |

The event fires for **every** call — success, non-2xx responses, and network failures alike. The existing [`cost:http:request`](/costs/cost-events#http-request-cost) event is unchanged and continues to fire only when you call `addRequestCost`.

## Attaching Request Cost

When you know the dollar cost of an HTTP request (for example from provider billing headers), you can attach it to the HTTP trace event with `addRequestCost`.

```typescript theme={null}
import { addRequestCost, createKyClient } from '@outputai/http';

const client = createKyClient( { prefix: 'https://api.vendor.com' } );

const response = await client.post( 'search', {
  json: { query: 'acme' }
} );

const inputCost = Number( response.headers.get( 'x-cost-input-usd' ) ?? 0 );
const outputCost = Number( response.headers.get( 'x-cost-output-usd' ) ?? 0 );

addRequestCost( response, inputCost + outputCost );

response.body?.cancel();
```

`addRequestCost` accepts the total request cost as a number.

`addRequestCost` only works with responses returned by `outputFetch` or `createKyClient`. If the response did not come from this package, the function safely no-ops and logs a warning.

It also emits a **`cost:http:request`** hook event (same hooks system as [`cost:llm:request`](/costs/cost-events)). For the payload and examples, see [Cost Events — HTTP](/costs/cost-events#http-request-cost).

## Error Handling

`outputFetch` returns non-2xx responses without throwing. Ky clients use Ky's configured error behavior and throw `ky.HTTPError` for non-2xx responses by default and `ky.TimeoutError` for timeouts:

```typescript theme={null}
import { createKyClient, ky } from '@outputai/http';

const client = createKyClient( { prefix: 'https://api.example.com' } );

try {
  const response = await client.get( 'users/1' );
  return response.json();
} catch (error) {
  if (error instanceof ky.HTTPError) {
    if (error.response.status === 404) {
      return null;
    }
    throw error;
  }
  if (error instanceof ky.TimeoutError) {
    throw error;
  }
  throw error;
}
```

Since steps retry automatically, you generally just need to handle cases where retrying won't help (like a 404). For everything else, let the error propagate and the step's retry policy will handle it.

## Undici Dispatchers

Requests use an `EnvHttpProxyAgent` by default, including standard HTTP proxy environment variables. You can provide an Undici dispatcher for one request:

```typescript theme={null}
import { outputFetch, undici } from '@outputai/http';

const dispatcher = new undici.Agent( {
  headersTimeout: 60_000,
  bodyTimeout: 60_000
} );

const response = await outputFetch( 'https://api.example.com', {
  dispatcher
} );
```

Set `dispatcher: undefined` explicitly to bypass the default dispatcher.

## Ky and Undici Exports

The complete Ky and Undici namespaces are re-exported for convenience and to let callers use the same Undici realm as `outputFetch`:

```typescript theme={null}
import { ky, undici } from '@outputai/http';

const request = new undici.Request( 'https://api.example.com' );
const isHttpError = ( error: unknown ) => error instanceof ky.HTTPError;
```

## Environment Variables

| Variable                    | Description                                                                                                                              |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `OUTPUT_TRACE_HTTP_VERBOSE` | Set to `true` or `1` to include request/response headers and bodies in trace files (otherwise only method, URL, and status are recorded) |

## API Reference

For complete TypeScript API documentation, see the [HTTP Module API Reference](https://output-ai-reference-code-docs.onrender.com/modules/http_src.html).
