Hooks
Observe a call as it runs — onRequest, onResponse, onError, onRetry — and where each fires relative to auth, retry, throttle, and timeout.
Hooks are observation points on a stitch's lifecycle: small callbacks that run as a
call progresses, for logging, metrics, or tracing. They never change what a stitch
returns — a call's only result is its response — so reach for a hook when you want
to watch a call, for transform when you want to
reshape its output, and for the
input schema when you want to
reshape what it sends.
Example
import { } from 'stitchapi';
const = ({
: 'https://api.example.com/users/{id}',
: {
: ({ , , }) => {
.(`→ ${} #${}: ${?.} ${?.}`);
},
: ({ }) => {
.(`← ${?.}`);
},
: ({ , , }) => {
.(`retrying after #${}`, ?? ?.);
},
},
});The four hooks
Each hook receives a HookContext — { name, attempt, req?, res?, error? } — with
the fields relevant to its moment populated:
| Hook | Fires when | Populated |
|---|---|---|
onRequest | just before the request goes out | req |
onResponse | a response came back (any status) | res |
onError | the transport threw (network/abort/timeout) | error |
onRetry | the engine decided to retry, before backoff | error or res |
onResponse fires on any status, which makes it the seam for everything a
server tells you on a call that succeeded. A vendor announcing a shutdown does
exactly that — Deprecation and Sunset ride a 200, so nothing fails and
nothing retries; the vendor told you for six months, in a header
finds the three places a response header is actually reachable.
Firing order
Hooks interleave with the engine's per-attempt pipeline. Within a single attempt, in order:
- throttle — wait for a concurrency / rate slot
auth.apply— credentials are stamped onto the requestonRequest— the request is fully shaped (auth headers included)- the network call (bounded by
timeout.each) - on a thrown transport error →
onError, then if another attempt remains →onRetry→ backoff → next attempt - on a response →
onResponse auth.shouldRefresh/auth.refresh— a token wall (e.g. a 401) refreshes auth and replays the attempt- on a retryable status →
onRetry→ backoff → next attempt
So onRequest always sees the post-auth request, and onRetry fires with error
populated on a network failure or res populated on a retryable status.
Hooks run on every attempt, not once per call: a stitch that retries
twice fires onRequest three times. Use attempt to tell them apart.
Observe the request; don't reshape it. ctx.req is the live object the
transport is about to send, so mutating it in onRequest does change the
call — but step 3 puts you after auth.apply, and a strategy that signs
the payload (AWS SigV4 with signBody, or any HMAC scheme) has already
hashed the body you are replacing. The signature then covers bytes that
never went out, and the API rejects the call with a 403 that points at
nothing. Reshape the request in its input
schema, which resolves
before auth signs anything — and before the cache key is derived from the
request, which hook mutation also misses.
Order across composed layers
When stitches compose via extends, hooks
chain rather than overwrite — every layer's hook runs. They unwind like
middleware:
onRequestruns base → child (outermost layer first)onResponse,onError,onRetryrun child → base (innermost layer first)
import { } from 'stitchapi';
const = ({
: 'https://api.example.com',
: { : () => .('base onRequest') },
});
const = ({
: [],
: '/users/{id}',
: { : () => .('child onRequest') },
});
// Calling getUser logs "base onRequest" then "child onRequest".