fetch API
fetch is a global function. It sends one HTTP request and resolves with a Response — including for HTTP error statuses like 404 or 500.
Quick look
Modules vs scripts
ff treats a file as an ES module only when its name ends in .mjs or
its first code is import/export. A plain .js file runs as a classic
script, where top-level await is a syntax error — so the runnable
examples on this page wrap async code in (async () => { ... })(). Save
any of them with a .mjs extension if you'd rather use top-level await
as written.
fetch(url, init?)
Only method / headers / body are read. These init keys are ignored: signal, timeout, redirect, credentials, cache, mode.
Response reference
The object fetch resolves with — and the object your http.serve handler returns.
Each body reads once:
Construction + statics (for handlers and tests):
Request reference
Build requests explicitly when you need to inspect or reuse them:
Headers reference
Case-insensitive; names are lowercased internally:
CR/LF characters in header values are dropped.
Redirects
Followed automatically up to 5 hops. res.redirected and res.url tell you where the response actually came from. Past 5 hops, fetch throws Too many redirects. There is no manual-redirect mode.
Note: redirects your own http.serve handler returns via Response.redirect() carry no Location header, so nothing follows them — see Gotchas.
Proxies
fetch honors the standard proxy environment variables. They are read once
at process start — set them before launching ff:
Proxy URLs may include credentials (http://user:pass@proxy:8080), which
are sent as proxy-authorization. http:// requests are tunneled with
CONNECT (falling back to an absolute-form request line when the proxy
refuses it); https:// targets always tunnel through CONNECT and carry
real end-to-end TLS to the target. NO_PROXY entries match the host itself
and its subdomains (example.com also matches api.example.com); append
:port to scope an entry, and * bypasses every host. Only HTTP proxies
are supported (no SOCKS), and the proxy URL itself must be http:// — an
https:// proxy URL (TLS to the proxy) is rejected.
Full example
Limits (summary)
Full table lives in the Limitations reference: 16 concurrent fetches, 64KB body stage per slot, 30s I/O timeout, 5 redirect hops, no abort/signal, HTTP/1.1 only, HTTP proxies via env vars only (no SOCKS).
Gotchas
Method names are exact-case. fetch matches init.method against the standard tokens verbatim — "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS". Anything else, including lowercase ("post"), is sent as GET (a body is still attached). Your http.serve handler likewise sees the method exactly as the client sent it, so normalize before comparing (method.toUpperCase() === "POST").
Only string bodies reach the wire intact. Every other body value is coerced with JS String() semantics: a FormData or Blob arrives as "[object Object]", an ArrayBuffer as "[object ArrayBuffer]", and a Uint8Array([104, 105]) as "104,105". URLSearchParams survives only because its toString() is the query string. There is no multipart encoding and no binary-body path — serialize yourself (JSON.stringify, params.toString(), base64, or a hand-built multipart string with an explicit content-type header).
Response.redirect() sends no Location header. It sets the status (default 302) and flags the object as redirected, but clients never follow it. Build the response yourself when clients must follow:
Rejections are plain strings. Network failures reject with "Network error", bad URLs with "Invalid URL", long chains with "Too many redirects" — e.message is undefined, so log String(e).
See also
- Fetch Client guide — tutorial version of this page
- HTTP Server guide — serving instead of calling
- Limitations — every hard cap
