js/globals/fetch
js/globals/fetch.ts
Fetch global and server-side transport.
This module installs the global fetch() function and contains the
server-side transport implementation. Some pool-inspection hooks exist for
runtime tests, but application code calls the global directly and never
imports this module.
Implements Fino's release-supported server-side Fetch baseline:
- HTTP and HTTPS support over plain TCP and TLS
- HTTP/2 reuse for HTTPS origins that negotiate h2 through ALPN
- HTTP/3 over QUIC, forced with protocol: 'h3' or discovered through
Alt-Svc response headers
- Redirect following with configurable redirect mode
- AbortSignal cancellation, including during response body streaming
- Subresource integrity checks for buffered response bodies
- Explicit referrer and referrerPolicy handling
- Response decompression for gzip, deflate, and brotli when available
- data: and blob: URL fetches resolved without touching the network
- global Request/Response/Headers backed by internal:net/http/wire
Connection lifecycle
HTTP/1 requests open a fresh TCP or TLS connection per hop and request
Connection: close. The connection is kept alive until the response body is
fully consumed (or the iterator is closed early), at which point the socket
is closed. 204/304 and other bodyless responses close the socket immediately
after parsing headers. HTTPS requests that negotiate HTTP/2 through ALPN can
reuse the resulting H2 session through the origin-keyed pool.
HTTP/3 and Alt-Svc
When libnghttp3 is available, protocol: 'h3' forces the request over QUIC.
In 'auto' mode an HTTPS response carrying an Alt-Svc: h3=... header
populates an origin-keyed cache (honouring the ma max-age parameter), and
later requests to that origin with replayable bodies (string, Uint8Array,
ArrayBuffer, or none) are first attempted over a pooled H3 session. If the
QUIC attempt fails, the cache entry is evicted and the request silently
falls back to TCP.
Redirect handling
The redirect init option controls redirect behaviour:
- 'follow' (default) — follow up to 20 redirects, then throw TypeError
- 'error' — throw TypeError on any redirect response
- 'manual' — return an opaque redirect response
(status 0, empty headers, null body)
Method changes on redirect: - 301, 302, 303: method → GET, body dropped - 307, 308: method kept; body must be replayable — a streaming (async-iterable or ReadableStream) body throws TypeError
A redirect status without a Location header is returned as a normal
response. Redirect targets must be http: or https: URLs; anything else
(javascript:, file:, data:, ...) throws TypeError.
AbortSignal
signal is raced against every async operation: DNS lookup, TCP connect,
TLS handshake, and response parsing. After fetch() returns, the wrapped
body iterator also checks signal.aborted on each .next() call so that
a long streaming response can be cancelled mid-stream.
Request safety
Requests to a known list of unsafe ports (SMTP, IRC, NFS, ...) are rejected
with a TypeError, mirroring the WHATWG bad-port list. The CONNECT,
TRACE, and TRACK methods are forbidden. GET and HEAD requests reject
when given a body.
Browser policy non-parity
Authorization, Cookie, and Cookie2 are stripped when following a
redirect to a different origin. Otherwise this is a server-side transport
API, not a browser policy engine. mode, credentials, cache, and
keepalive are accepted as compatibility fields, but they do not enforce
CORS, create opaque no-cors responses, maintain a browser cookie jar, reuse
cached responses, extend upload lifetime after shutdown, or synthesize a
default browser referrer. Caller-provided Cookie and authorization headers
remain explicit request headers until a cross-origin redirect strips them.
Usage
const res = await fetch('https://example.com/data');
const json = await res.json();
const posted = await fetch('https://example.com/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'widget' }),
signal: AbortSignal.timeout(5000),
redirect: 'follow',
});
WHATWG Fetch Standard: https://fetch.spec.whatwg.org/
Interfaces
interface FetchInit {
Options accepted by the global fetch() function.
Fino accepts the standard request init fields used by browser and server
Fetch implementations, plus transport-specific tls, protocol, and
request trailers options for runtime-managed HTTP clients. Fields set
here override the corresponding fields of a Request passed as input.
const res = await fetch('https://internal.example/report', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ok: true }),
signal: AbortSignal.timeout(10_000),
redirect: 'error',
tls: { ca: '/etc/ssl/internal-ca.pem' },
protocol: 'h2',
});
Properties
method?: string
Request method. Common methods are normalized to uppercase before
sending; unknown methods are sent verbatim. CONNECT, TRACE, and
TRACK throw a TypeError.
headers?: Headers | string[][] | Record<string, string> | null | undefined
Request headers as a Headers instance, an array of [name, value]
pairs, or a plain name-to-value record. Host, Connection, and
Accept-Encoding are auto-injected for HTTP/1 requests when absent.
body?: unknown
Request body value: a string, byte buffer, Blob, FormData,
URLSearchParams, async iterable of Uint8Array, or ReadableStream.
Streaming bodies are one-shot — they cannot be replayed across 307/308
redirects and disable automatic Alt-Svc HTTP/3 upgrades. GET and HEAD
requests must not have a body.
signal?: AbortSignal | null
Abort signal used to cancel connection setup, redirects, uploads, and
streaming response reads. Rejects with signal.reason.
redirect?: 'follow' | 'error' | 'manual'
Redirect handling mode. 'follow' (default) chases up to 20 redirects,
'error' throws a TypeError on any redirect status, and 'manual'
returns an opaque redirect response (status 0, empty headers, null body).
integrity?: string
Subresource integrity metadata (e.g. sha256-<base64>; sha256, sha384,
and sha512 are supported). The response body is buffered in full and the
digest verified before the Response is returned; a mismatch throws a
TypeError. Silently skipped when libcrypto is unavailable.
referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' | ''
Referrer policy used when constructing the outgoing Referer header
from referrer. Defaults to 'strict-origin-when-cross-origin'. Only
applies when an explicit referrer is provided — Fino never synthesizes
a default browser referrer.
referrer?: string
Explicit referrer URL reduced through referrerPolicy into a Referer
header. 'no-referrer' suppresses the header; 'about:client' is
ignored (no header is sent).
mode?: 'cors' | 'no-cors' | 'same-origin' | 'navigate'
Browser compatibility field. Fino accepts it but does not enforce CORS or synthesize opaque responses.
credentials?: 'omit' | 'same-origin' | 'include'
Browser compatibility field. Fino does not maintain a browser cookie jar.
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached'
Browser compatibility field. Fino does not maintain an HTTP cache.
keepalive?: boolean
Browser compatibility field. Fino accepts it without extending request lifetime after runtime shutdown.
priority?: 'high' | 'low' | 'auto'
Request priority hint. Validated ('high', 'low', or 'auto') but not
currently used for scheduling.
trailers?: Headers | (() => Headers | Promise<Headers>)
HTTP trailers sent after the request body: either a Headers instance
or a function (sync or async) called after the body is written, so
trailer values can be computed from the streamed content.
tls?: {
ca?: string;
rejectUnauthorized?: boolean;
cert?: string;
key?: string;
}
TLS client and trust options for HTTPS connections. ca, cert, and
key are paths to PEM files; rejectUnauthorized defaults to true.
Requests carrying a client certificate are pooled separately from
anonymous connections to the same origin.
protocol?: 'auto' | 'http/1.1' | 'h2' | 'h3'
Preferred HTTP protocol. 'auto' (default) negotiates: ALPN picks h2 or
HTTP/1.1, and a cached Alt-Svc entry may upgrade to h3. 'h2' requires
ALPN to negotiate h2 and throws otherwise; 'h3' requires an HTTPS URL
and libnghttp3. 'http/1.1' pins the request to HTTP/1.
Functions
async function fetch(input: string | Request, init?: FetchInit): Promise<Response>
Fetch a resource over HTTP or HTTPS.
Follows the WHATWG Fetch API core flow with Fino Request, Response, and
Headers objects. input is a URL string (resolved against
globalThis.location when relative and a location is set) or a Request
whose fields serve as defaults for init. data: and blob: URLs are
resolved in-process without network I/O; blob: fetches support GET only,
including Range requests. The returned Response carries the final URL
after redirects and redirected metadata.
Redirect mode defaults to 'follow', up to 20 hops, and cross-origin
redirects strip Authorization and Cookie headers. GET and HEAD requests
reject when given a body. Requests to unsafe ports and the CONNECT,
TRACE, and TRACK methods throw a TypeError. AbortSignal cancellation is
checked before the request starts, during DNS/connect/TLS/parse, and while
reading the response body.
The returned Response may have a streamed body. If the body is not consumed
or closed, the underlying HTTP/1 socket can remain open until the runtime
tears it down. HTTPS requests may reuse an HTTP/2 session when ALPN
negotiates h2, or an HTTP/3 session when protocol: 'h3' is set or a
cached Alt-Svc entry exists. Integrity checks buffer the full body before
returning a Response. Compressed bodies (gzip, deflate, brotli) are
transparently decompressed and the Content-Encoding/Content-Length
headers removed.
Browser policy knobs are intentionally limited in this release: CORS,
credentials, cache, cookies, keepalive lifetime, and default referrer
behavior are not enforced by the runtime. mode: "no-cors" still returns a
normal response, credentials never creates an implicit cookie jar, cache
never reuses a prior response, and keepalive does not extend work beyond
normal runtime lifetime. Explicit referrer and referrerPolicy values are
converted to a Referer header when supported.
const response = await fetch('https://example.com/data.json', {
headers: { accept: 'application/json' },
signal: AbortSignal.timeout(5000),
redirect: 'follow',
});
if (response.ok) {
const payload = await response.json();
console.log(payload);
}
// Stream a large download, cancellable mid-body:
const controller = new AbortController();
const download = await fetch('https://example.com/archive.bin', {
signal: controller.signal,
});
let received = 0;
for await (const chunk of download.body!) {
received += chunk.byteLength;
if (received > 100_000_000) controller.abort(new Error('too large'));
}