Hooks

Hooks are registered on a client, run in registration order, and may be synchronous or async. They are not per-request configuration options.

import { Http } from "fetchzen";
const api = Http.create("https://api.example.com", {
cache: false,
hooks: {
request: [
({ url, init }) => {
init.headers.set("X-Request-Id", crypto.randomUUID());
return { url, init };
},
],
response: [
(response) => {
console.info("HTTP status", response.status);
},
],
error: [
(error) => {
console.error(error.message);
},
],
},
});
await api.get("/users");
HookReceivesReturn value
request{ url, init }, after headers, body, and URL preparationA context replaces URL/init; a Response skips the network and remaining request hooks.
response(response, context), before parsing and preset validationA Response replaces the response used by later hooks and parsing.
error(error, context)An Error replaces the error thrown to the caller.

Returning nothing keeps the current context, response, or error. Error hooks do not recover a successful result; the request still rejects (or returns the error slot through safe). Request hooks run once, not on each retry. Response hooks see the final response, not intermediate retry responses.

Using the client above, hook() returns an unsubscribe function. Remove temporary hooks when their owner is disposed to avoid duplicate handlers on long-lived clients.

const off = api.hook("request", ({ init }) => {
init.headers.set("X-Feature", "preview");
});
await api.get("/users");
off();

Each unsubscribe removes only its own registration, even if the same callback was registered more than once. Calling it again is a no-op. Requests already in progress keep their original hook snapshot.

Shared Http.create lookups reuse the same client and hooks; later initialization does not replace or append hooks. Use hook() and unsubscribe, not configure({ hooks: ... }). Each request snapshots defaults and all hook lists at its start, including response/error hooks. Changes affect later requests only, not mutable state in callback closures. For per-user SSR hooks, use private cache: false clients, not module/global shared state.

A request hook can return a normal Fetch Response. Response hooks, parsing, preset validation, and schema validation still run:

const offLocal = api.hook("request", ({ url }) => {
if (new URL(url).pathname === "/health") {
return Response.json({ data: { healthy: true } });
}
});
await api.get("/health");
offLocal();

Create a fresh response or clone a stored one for each request; a consumed response cannot be reused. This is a transport override, not built-in response caching.

Logging is separate from hooks and off by default. Enable it with log: true on the instance or request. A custom logger is configured on the instance:

import { Http } from "fetchzen";
const api = Http.create("https://api.example.com", {
cache: false,
log: true,
logger: {
request: ({ init }) => console.debug(init.method),
response: ({ response, duration }) => console.debug(response.status, duration),
error: ({ error }) => console.error(error.name),
},
});
await api.get("/health");

duration is milliseconds through body parsing, not just network latency. A response log occurs before preset/schema validation, so it can be followed by an error log.

The built-in logger redacts authorization, proxy-authorization, cookie, set-cookie, x-api-key, and x-auth-token headers. URLs, bodies, and error details are not redacted. Custom loggers receive unredacted data; use redactHeaders when logging headers and apply your own policy to other fields.

See errors for error identity and retries for retry behavior. Try the focused hooks example.