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:
methoddefaults toGET.headersis a plain object or an array of[name, value]pairs.bodymust be a string, soJSON.stringifyyour payload yourself and set acontent-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.
| 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 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".