onRequestError(ctx.error)rejectonRequestError receives the original error and may replace ctx.error before rejection.
Hooks share request and response behavior without coupling the HTTP client to a router, UI library, or state store.
onRequestmutate the request before normalizationadapterexecute the transportonResponsemutate the parsed responsepolicyapply the project response policyonRequestError(ctx.error)→rejectonRequestError receives the original error and may replace ctx.error before rejection.
kind: ok→onSuccess(ctx.value)→resolvekind: non-ok→onResponseError(ctx.error)→rejectA non-ok result creates the classified error. onResponseError may replace ctx.error before rejection.
resolve | reject→onFinally(ctx)→settle PromiseonFinally runs for resolved and rejected requests. Request plus cleanup failures produce AggregateError.
onRequestError handles adapter throws. onResponseError runs after response policy classifies non-ok. onSuccess receives the value selected for resolution. onFinally runs once on every path.
Hooks run sequentially. Return values are ignored; mutate ctx.request, ctx.response, ctx.error, or ctx.value at the matching phase.
const http = createHttp({
adapter: "fetch",
hooks: {
onRequest: [auth(getToken), addLocale, addTraceId],
onRequestError: reportNetworkFailure,
onResponse: normalizeSharedHeaders,
onResponseError: [handleUnauthorized, showErrorToast],
onSuccess: observeResolvedValue,
onFinally: stopTrace,
},
});const http = createHttp({
adapter: "fetch",
hooks: {
onSuccess: (ctx) => {
ctx.value = normalizeResolvedValue(ctx.value);
},
onFinally: (ctx) => {
finishTrace(ctx.request, ctx.error);
},
},
});onFinally hooks all run even when an earlier cleanup hook fails. If the request and cleanup both fail, the promise rejects with an AggregateError: request error first, cleanup errors after it in declaration order.
ignoreResponseError: true suppresses rejection, not classification. onResponseError, onSuccess, and onFinally all receive the classified ctx.error while the caller receives the response body.
Use local hooks for one endpoint instead of adding URL branches to a global hook.
await http.get("/legacy/report", {
hooks: {
onRequest: (ctx) => {
ctx.request.headers["X-Format"] = "legacy";
},
onResponse: (ctx) => {
ctx.response.body = normalizeLegacyReport(ctx.response.body);
},
},
});Global hooks run before request-local hooks.
import { auth, createHttp } from "@envoijs/http";
const http = createHttp({
adapter: "fetch",
hooks: {
onRequest: auth(() => localStorage.getItem("token")),
},
});Existing Authorization headers are not overwritten.
The silent option is stored in request metadata for an error hook to read. ctx.error is already classified as BizError for HTTP and envelope failures.
import { BizError } from "@envoijs/http";
const http = createHttp({
adapter: "fetch",
hooks: {
onResponseError: (ctx) => {
if (ctx.error instanceof BizError && ctx.request.meta.silent !== true)
showError(ctx.error.msg);
},
},
});
await http.get("/background-check", { silent: true });The same error is thrown after the hook, so stores and query libraries observe failure.
To package several phases together, see Reusable middleware.
Hook return values cannot replace a failed request. Token refresh needs an explicit outer request wrapper or an adapter feature. Use one shared refresh promise and a one-retry guard to prevent refresh storms and infinite recursion.
Use hooks for auth, locale, time zone, tracing, logging, shared normalization, and global errors. Keep endpoint data writes in the API, store action, atom, or query that owns that request.