# fetch

> A host-bridged HTTP client with SSRF guards, limits, and predictable error handling

`fetch` makes an HTTP(S) request and returns the response. Your app code runs in a sandbox with no network, so `fetch` runs the request in the trusted host and hands you back the result. It is **not** the standard `fetch`.

```js

const res = await fetch('https://catfact.ninja/fact');
if (res.status === 200) {
  const { fact } = JSON.parse(res.body);
  conn.write(fact + '\r\n');
}
```

## Signature

```ts
function fetch(url: string, opts?: FetchOptions): Promise<FetchResult>

interface FetchOptions {
  method?: string;                                       // default "GET"
  headers?: Record<string, string> | Array<[string, string]>;
  body?: string;                                         // must be a string
}

interface FetchResult {
  status: number;                       // e.g. 200, 404
  headers: Record<string, string>;      // header names lower-cased
  body: string;                         // UTF-8 text; parse JSON yourself
}
```

Only `method`, `headers`, and `body` cross into the host:

- `method` defaults to `GET`.
- `headers` is a plain object or an array of `[name, value]` pairs.
- `body` must be a **string**, so `JSON.stringify` your payload yourself and set a `content-type`.

The result `body` is always a UTF-8 string. There is no streaming and no `.json()` helper, so call `JSON.parse(res.body)` yourself. Response header names are lower-cased.

## Examples

```js
// GET and parse JSON
const res = await fetch('https://api.example.com/items/42');
if (res.status === 200) {
  const item = JSON.parse(res.body);
  conn.write('item: ' + item.name + '\r\n');
}
```

```js
// POST JSON
const res = await fetch('https://api.example.com/items', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'widget' }),
});
```

## Status codes do not throw

A non-2xx response does **not** throw. You get the status back and decide what to do with it. `fetch` only rejects if the request is blocked, times out, exceeds a limit, or otherwise fails, and the error is sanitized so no host details leak into the sandbox.

```js
try {
  const res = await fetch('https://api.example.com/data');
  if (res.status >= 400) {
    conn.write('  (server returned ' + res.status + ')\r\n');
    return;
  }
  conn.write(res.body + '\r\n');
} catch (err) {
  conn.write('  (fetch failed: ' + err.message + ')\r\n');
}
```

<Callout title="Tip">
Always `try/catch` your `fetch` calls and render a fallback. Network failures and rate limits are normal, and a good terminal app degrades gracefully instead of going blank.
</Callout>

## Limits and guards

Your app controls the URL and the request runs from the host's network position, so `fetch` is an SSRF surface and is heavily guarded host-side. These limits apply **per app**.

| Guard | Limit |
| --- | --- |
| Schemes allowed | `http:` and `https:` only |
| Blocked targets | private, loopback, link-local, and cloud-metadata addresses are refused, IP literals included |
| DNS rebinding | the request is pinned to the validated address |
| Redirects | up to **5** hops, each one re-validated |
| Cross-origin redirects | credential headers are stripped |
| Request timeout | **10 seconds** |
| Response size | **5 MiB** cap |
| Rate limit | **30** requests per **10 second** window per app |
| Concurrency | up to **6** in-flight requests |

For the full reasoning behind these guards, see [Limits](/platform/limits) and [Security](/platform/security).

## Availability

`fetch` only works inside the voxal runtime, which provides the host bridge. Calling it elsewhere (for example a unit test that forgot to stub the host) throws *"fetch() is only available inside the voxal runtime"*.

---

Source: https://docs.voxal.sh/sdk/fetch
