voxal docs
SDK · Core

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.

import { fetch } from '@voxalsh/sdk';

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

Signature

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

// 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');
}
// 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.

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');
}

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.

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.

GuardLimit
Schemes allowedhttp: and https: only
Blocked targetsprivate, loopback, link-local, and cloud-metadata addresses are refused, IP literals included
DNS rebindingthe request is pinned to the validated address
Redirectsup to 5 hops, each one re-validated
Cross-origin redirectscredential headers are stripped
Request timeout10 seconds
Response size5 MiB cap
Rate limit30 requests per 10 second window per app
Concurrencyup to 6 in-flight requests

For the full reasoning behind these guards, see Limits and 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".

On this page