Fetch data from an API
Call external HTTP APIs from a voxal app with the host-bridged fetch, including JSON, errors, and limits
Voxal apps run inside a sandbox with no network access of their own. To reach the outside world you import fetch from @voxalsh/sdk, which runs the request in the trusted host and hands the result back to your app. This guide covers GET and POST, parsing JSON, handling errors, showing a loading state, and the limits you need to design around.
The shape of fetch
import { fetch } from '@voxalsh/sdk';
const res = await fetch(url, { method, headers, body });
// res: { status, headers, body }Two things differ from the browser fetch:
- The request
body, if present, must be a string. Serialize JSON yourself withJSON.stringify. - The result
bodyis always a UTF-8 string, not a stream or a parsed object. CallJSON.parse(res.body)to get JSON.
A GET request
import { createApp, fetch } from '@voxalsh/sdk';
createApp()
.onKey(async (conn, data) => {
if (data !== 'c') return;
try {
const res = await fetch('https://catfact.ninja/fact');
const fact = JSON.parse(res.body).fact;
conn.write('🐱 ' + fact + '\r\n');
} catch (err) {
conn.write('(fetch failed: ' + err.message + ')\r\n');
}
})
.listen();The SDK does not await your handlers, so always wrap an await fetch(...) in try/catch. An unhandled rejection is logged to the server logs, not shown to the user, so they would see nothing happen.
A POST request with JSON
Set the method, headers, and a stringified body.
const res = await fetch('https://api.example.com/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'widget', qty: 3 }),
});Handling non-2xx responses
A non-2xx status does not throw. Only network-level failures (timeout, blocked address, transport error) reject the promise. Check res.status yourself.
const res = await fetch('https://api.example.com/data');
if (res.status >= 200 && res.status < 300) {
const data = JSON.parse(res.body);
// use data
} else {
conn.write('request failed with status ' + res.status + '\r\n');
}A loading state in a UI app
In the UI layer, track a busy flag in state, set it before the request, and clear it in a finally so it resets even on error.
import React, { useState } from 'react';
import { serve, Box, Text, useInput } from '@voxalsh/sdk/ui';
import { fetch } from '@voxalsh/sdk';
function Fact() {
const [fact, setFact] = useState('press f for a cat fact');
const [busy, setBusy] = useState(false);
const load = async () => {
setBusy(true);
try {
const res = await fetch('https://catfact.ninja/fact');
setFact(JSON.parse(res.body).fact);
} catch (err) {
setFact('(failed: ' + err.message + ')');
} finally {
setBusy(false);
}
};
useInput((input) => {
if (input === 'f' && !busy) load();
});
return (
<Box borderStyle="round" paddingX={1}>
<Text>{busy ? '…fetching' : fact}</Text>
</Box>
);
}
serve(() => <Fact />);Limits
fetch runs from the host, so it is guarded and rate-limited. Design for these:
| Limit | Value |
|---|---|
| Schemes | http and https only |
| Blocked targets | private, loopback, link-local, and cloud-metadata addresses |
| Redirects | up to 5 hops |
| Timeout | 10 seconds per request |
| Response size | 5 MiB cap |
| Rate | 30 requests per 10 seconds per app |
| Concurrency | 6 in flight per app |
The blocked-address rules cannot be turned off; they exist so that a deployed app cannot probe the host's internal network. If you need a request to a private service, it has to be reachable on a public address.