Features & request pipeline
This page lists what openFetch ships today and shows the end-to-end request pipeline after merging config, lifecycle hooks, middleware (including retry), and dispatch internals.
For stack comparisons and deep dives, see Architecture & internals. For option-by-option reference, see Configuration.
Package overview
| Topic | Detail |
|---|---|
| npm | @hamdymohamedak/openfetch |
| Module format | ESM only ("type": "module") |
| Runtime deps | Zero |
| Engines | Node 18+; also browsers, Bun, Deno, Cloudflare Workers, and other fetch-capable edges |
| Entrypoints | Main package; /plugins (retry, timeout, hooks, debug, strictFetch); /sugar (createFluentClient) |
| Transport | Native fetch only (no XHR adapter) |
Feature inventory
Client & config
createClient/create— instance withdefaults, HTTP verb helpers,request(),use(middleware).mergeConfig— merges defaults + per-call: shallow top-level keys; concatenatesmiddlewares,transformRequest,transformResponse,init; composesretry.onBeforeRetry/retry.onAfterResponse; shallow-merge forretry/memoryCache; shallowheaders; strips prototype-pollution keys.init— synchronous callbacks on the merged config before interceptors (mutate headers, URL-related fields, etc.).- Native
Requestinput —client.request(request, overrides?)mergesRequestURL, method, headers, body, signal, andRequestInitpassthrough fields with defaults and overrides. throwHttpErrors— Ky-style optional gate whenvalidateStatusis omitted:falsenever throws on HTTP status; function(status) => truemeans “throw for this status”. IfvalidateStatusis set, it wins andthrowHttpErrorsis ignored.
Interceptors & middleware
- Request interceptors — async chain on config (LIFO order).
- Response interceptors — async chain on
OpenFetchResponse(FIFO order). - Middleware —
(ctx, next) => Promise<void>around the innernext()that eventually callsdispatch; only the innermostnextinvokesfetch.
Transport (dispatch)
- URL —
buildURLwithbaseURL,params, optionalassertSafeUrl(SSRF-style guard; not a full egress firewall). - Headers — normalized to lowercase keys; optional
auth(Basic); suggestedAcceptwhenresponseTypeis set andacceptis absent (application/jsonforjson, etc.). transformRequest— ordered(data, headers) => …before body serialization.- Body — JSON default for plain objects; respects
FormData,Blob, etc. timeout— internalAbortControllermerged withsignal; on timeout alone →OpenFetchErrorcodeERR_TIMEOUT(user abort onconfig.signal→ERR_CANCELED).fetch— single call with mergedRequestInitfields.rawResponse— skip parse +transformResponse;datais nativeResponse(interceptors still run).
After fetch (normal path, not rawResponse)
- Parse body — by
responseTypeorContent-Typeheuristic. validateStatus— failure →ERR_BAD_RESPONSEwith parsed body attached when applicable.jsonSchema— optional Standard Schema validation of parsed JSON; failure →SchemaValidationError(not anOpenFetchError).transformResponse— ordered transforms on successful pipeline.
Retry (createRetryMiddleware / retry() plugin)
- Exponential backoff, jitter, status-based retry, network/parse retry rules, POST idempotency key when retrying non-idempotent methods (opt-in), monotonic total budget (
timeoutTotalMs), per-attempt timeout override. retry.onBeforeRetry— after a failed attempt, before backoff (when another attempt may run).retry.onAfterResponse— after a successful inner round-trip producedctx.response; throwOpenFetchForceRetryto force another attempt (handled inside the retry loop).hooks()plugin — can setonBeforeRetry/onAfterResponse; they are merged intoctx.request.retrywith any existing handlers.
Cache, plugins, fluent
- Memory cache middleware — TTL, stale-while-revalidate, vary headers on cache key (see Retry & cache).
- Plugins —
timeout,hooks,debug,strictFetch(stricter redirect default when unset). - Fluent —
createFluentClient→fluent(url).get().json()/.text()/.send()/.raw()/.json(schema)(Standard Schema), optional.memo()for one buffered round-trip.
Errors & utilities
OpenFetchErrorwithcode, optionalresponse,toShape()/toJSON()for safer logs (redacts URL query by default).- Guards —
isOpenFetchError,isHTTPError,isTimeoutError,isSchemaValidationError. - Utilities —
assertSafeHttpUrl,maskHeaderValues,redactSensitiveUrlQuery, idempotency helpers,cloneResponse.
Full pipeline (one client call)
High level: merge → init → request interceptors → middleware stack (often retry wraps inner) → dispatch → response interceptors → return.
flowchart TD
M[mergeConfig] --> I[init hooks sync]
I --> RI[Request interceptors]
RI --> MW[Middleware stack]
MW --> RO[Response interceptors]
RO --> OUT[Return OpenFetchResponse or unwrap data]Inside middleware (when createRetryMiddleware is registered): each attempt runs await next() so inner middleware + dispatch execute once per try. On success, retry.onAfterResponse runs; if it throws OpenFetchForceRetry, the attempt is treated as retryable and the loop continues. On failure, retry.onBeforeRetry may run, then backoff, then the next attempt if policy allows.
Inside dispatch (each successful fetch for non-rawResponse)
Order is fixed in source (openFetch/src/transport/dispatch.ts):
flowchart TD
A[Resolve URL: buildURL + assertSafeUrl optional] --> B[Headers: normalize + suggested Accept + auth]
B --> C[transformRequest chain]
C --> D[Serialize body + mergeAbortSignals + fetch]
D --> E{rawResponse?}
E -->|yes| VS1[validateStatus on status only] --> Z[Return OpenFetchResponse with native Response as data]
E -->|no| P[parseBody]
P --> VS2[validateStatus — may throw ERR_BAD_RESPONSE with parsed body]
VS2 --> JV{jsonSchema set?}
JV -->|yes| SCHEMA[Standard Schema validate — may throw SchemaValidationError]
JV -->|no| TR
SCHEMA --> TR[transformResponse chain]
TR --> Z2[Return OpenFetchResponse]ASCII (quick copy)
mergeConfig (defaults + per-call, Request merged if used)
↓
init[] (sync, mutate merged config)
↓
request interceptors (async, LIFO)
↓
middleware stack (outer first)
↓
┌─ retry middleware (if registered): loop attempts
│ ↓
│ inner middleware…
│ ↓
│ dispatch: URL → headers → transformRequest → fetch
│ ↓
│ [raw?] validateStatus → return Response as data
│ OR
│ parse body → validateStatus → jsonSchema? → transformResponse
│ ↓
│ onAfterResponse (retry hook) — throw OpenFetchForceRetry? → loop
│ ↓
└─ (on failure before next attempt) onBeforeRetry → backoff → loop
↓
response interceptors (async, FIFO)
↓
return (unwrap data if unwrapResponse)Notes
onAfterResponseruns afterdispatchhas returned a fullOpenFetchResponse(includingtransformResponse), so the hook sees finaldataunless you userawResponse(thendatais still the nativeResponse).- Force retry is implemented by throwing
OpenFetchForceRetryfromonAfterResponse; the retry middleware treats it like a retryable failure and runsonBeforeRetrybefore backoff when applicable. - Middleware above retry in the stack runs once per outer client call; middleware below retry runs once per attempt.
Related docs
- Configuration — every
OpenFetchConfigfield - Interceptors & middleware — ordering mental model
- Retry & cache — retry options and cache semantics
- Errors & security — codes, guards, logging
- Plugins & fluent API — subpath imports
