Hooks
Rezo’s hooks system provides 26 hook points spanning the entire HTTP request lifecycle. Every hook is an array of functions, allowing multiple handlers per hook. Hooks are the foundation that interceptors are built on — they give you fine-grained control over every phase from DNS resolution to cookie processing.
Quick Start
import { Rezo } from 'rezo';
const client = new Rezo({
hooks: {
beforeRequest: [
(config, context) => {
console.log(`Request #${context.retryCount} to ${config.url}`);
}
],
afterResponse: [
(response, config, context) => {
console.log(`${response.status} in ${Date.now() - config.timing.startTime}ms`);
return response;
}
]
}
}); Hook Execution Types
Each hook uses one of five execution models. Understanding these is key to writing correct hooks.
Void Hooks
Run each handler in sequence. Handlers do not return a value. If any handler throws, execution stops.
Runner: runVoidHooks() (async) / runVoidHooksSync() (sync)
Hooks: init (sync), beforeRequest, beforeRedirect, beforeRetry, afterHeaders, afterCookie
hooks: {
beforeRequest: [
async (config, context) => {
// Mutate config in place, return nothing
config.headers.set('X-Request-ID', crypto.randomUUID());
}
]
} Transform Hooks
Each handler receives the previous handler’s output and returns a transformed value. The final result is used by the caller.
Runner: runTransformHooks()
Hooks: afterResponse, beforeError, afterParse
hooks: {
afterResponse: [
(response, config, context) => {
// Must return the response (original or modified)
response.data = response.data.results;
return response;
}
]
} Boolean Hooks
Each handler can return false to veto an action. If any handler returns false, the action is cancelled. Returning void or true allows it to proceed.
Runner: runBooleanHooks() (sync) / runBooleanHooksAsync()
Hooks: beforeCache, beforeCookie, beforeProxyDisable
hooks: {
beforeCache: [
(event) => {
// Return false to prevent caching this response
if (event.url.includes('/real-time/')) return false;
}
]
} Early Return Hooks
Handlers can return a value to short-circuit execution. The first non-void return value is used and remaining handlers are skipped. If all handlers return void, execution continues normally.
Runner: runEarlyReturnHooks()
Hooks: beforeRequest (can return a Response to bypass the adapter), beforeProxySelect (can return a ProxyInfo to override selection)
hooks: {
beforeRequest: [
(config, context) => {
// Return a Response to skip the actual HTTP request
if (config.url === '/health') {
return new Response('OK', { status: 200 });
}
// Return void to let the request proceed normally
}
]
} Event Hooks
Fire-and-forget handlers. Errors are caught and logged but never propagate — event hooks cannot break the request flow.
Runner: runEventHooks()
Hooks: onSocket, onDns, onTls, onTimeout, onAbort
hooks: {
onDns: [
(event, config) => {
// Errors here are caught and logged, never thrown
metrics.recordDnsLookup(event.hostname, event.duration);
}
]
} All 26 Hooks
Core Lifecycle Hooks
init
Called synchronously during options initialization. Use to normalize or validate request options before anything else runs.
type InitHook = (plainOptions: Partial<RezoRequestConfig>, options: RezoRequestConfig) => void; hooks: {
init: [
(plainOptions, options) => {
// Force JSON response type for API requests
if (options.url?.toString().includes('/api/')) {
options.responseType = 'json';
}
}
]
} Execution: sync void | Can throw: yes
beforeRequest
Called before the request is sent to the adapter. Can modify the config, add headers, or sign requests. Can return a Response to bypass the actual request entirely.
type BeforeRequestHook = (
config: RezoConfig,
context: BeforeRequestContext
) => void | Response | Promise<void | Response>; The context provides:
retryCount— 0 for initial request, 1+ for retriesisRedirect— whether this is a redirect follow-upredirectCount— number of redirects followed so farstartTime— timestamp when request processing started
hooks: {
beforeRequest: [
async (config, context) => {
// Sign the request with HMAC
const signature = await signRequest(config.method, config.url, config.data);
config.headers.set('X-Signature', signature);
}
]
} Execution: async early-return | Can throw: yes
beforeRedirect
Called before following an HTTP redirect. Use to inspect or modify redirect behavior, strip sensitive headers on cross-domain redirects, or log redirect chains.
type BeforeRedirectHook = (
context: BeforeRedirectContext,
config: RezoConfig,
response: RezoResponse
) => void | Promise<void>; The BeforeRedirectContext provides:
redirectUrl— the URL being redirected tofromUrl— the URL being redirected fromstatus— the HTTP status code (301, 302, 303, 307, 308)headers— response headers from the redirect responsesameDomain— whether the redirect stays on the same domainmethod— HTTP method of the current requestbody— the original request bodyrequest— the full request configurationredirectCount— number of redirects followed so fartimestamp— event timestamp
hooks: {
beforeRedirect: [
(context, config) => {
// Strip auth header on cross-domain redirects
if (!context.sameDomain) {
config.headers.delete('Authorization');
}
console.log(`Redirect ${context.status}: ${context.fromUrl} -> ${context.redirectUrl}`);
}
]
} Execution: async void | Can throw: yes
beforeRetry
Called before a retry attempt. Use for custom backoff logic, logging, or to prepare the next attempt.
type BeforeRetryHook = (
config: RezoConfig,
error: RezoError,
retryCount: number
) => void | Promise<void>; hooks: {
beforeRetry: [
async (config, error, retryCount) => {
console.log(`Retry ${retryCount}: ${error.code} - ${error.message}`);
// Refresh auth token before retrying
if (error.response?.status === 401) {
const token = await refreshToken();
config.headers.set('Authorization', `Bearer ${token}`);
}
}
]
} Execution: async void | Can throw: yes
afterResponse
Called after the response is received. Use to transform responses, unwrap API envelopes, or trigger retries via context.retryWithMergedOptions. Must return the response.
type AfterResponseHook<T = any> = (
response: RezoResponse<T>,
config: RezoConfig,
context: AfterResponseContext
) => RezoResponse<T> | Promise<RezoResponse<T>>; The AfterResponseContext provides:
retryCount— current retry countretryWithMergedOptions(options)— call to retry the request with merged options (throws internally to abort current flow)
hooks: {
afterResponse: [
(response, config, context) => {
// Token refresh pattern
if (response.status === 401 && context.retryCount === 0) {
const newToken = refreshTokenSync();
context.retryWithMergedOptions({
headers: { Authorization: `Bearer ${newToken}` }
});
}
return response;
}
]
} Execution: async transform | Can throw: yes
beforeError
Called before an error is thrown. Use to transform errors, add context, or return custom error subclasses. Must return an error object.
type BeforeErrorHook = (
error: RezoError | Error
) => RezoError | Error | Promise<RezoError | Error>; hooks: {
beforeError: [
(error) => {
// Enrich error with request ID
if ('config' in error && error.config?.requestId) {
error.message = `[${error.config.requestId}] ${error.message}`;
}
return error;
}
]
} Execution: async transform | Can throw: yes
Caching Hooks
beforeCache
Called before caching a response. Return false to prevent caching. The CacheEvent provides parsed Cache-Control directives and a default isCacheable assessment.
type BeforeCacheHook = (event: CacheEvent) => boolean | void; The CacheEvent provides:
status— HTTP status codeheaders— response headersurl— the request URLcacheKey— generated cache keyisCacheable— whether the response is cacheable by default rulescacheControl— parsed Cache-Control directives (maxAge,sMaxAge,noCache,noStore,mustRevalidate,private,public)
hooks: {
beforeCache: [
(event) => {
// Never cache user-specific data
if (event.url.includes('/me') || event.cacheControl?.private) {
return false;
}
}
]
} Execution: sync boolean | Can throw: no (false = veto)
Response Processing Hooks
afterHeaders
Called when response headers are received but before the body is read. Use to inspect headers, abort early, or prepare for response processing.
type AfterHeadersHook = (
event: HeadersReceivedEvent,
config: RezoConfig
) => void | Promise<void>; The HeadersReceivedEvent provides:
status,statusText— HTTP statusheaders— response headerscontentType,contentLength— parsed header valuesttfb— time to first byte in millisecondstimestamp— event timestamp
hooks: {
afterHeaders: [
(event, config) => {
console.log(`TTFB: ${event.ttfb}ms, Content-Type: ${event.contentType}`);
if (event.contentLength && event.contentLength > 100_000_000) {
throw new Error('Response too large');
}
}
]
} Execution: async void | Can throw: yes
afterParse
Called after the response body is parsed. Use to transform parsed data or validate the response structure. Must return the (possibly transformed) data.
type AfterParseHook<T = any> = (
event: ParseCompleteEvent<T>,
config: RezoConfig
) => T | Promise<T>; The ParseCompleteEvent provides:
data— the parsed datarawData— original raw data before parsingcontentType— Content-Type that triggered parsingparseDuration— how long parsing took in millisecondstimestamp— event timestamp
hooks: {
afterParse: [
(event, config) => {
// Validate JSON schema
if (!isValidSchema(event.data)) {
throw new Error('Invalid response schema');
}
// Transform: strip metadata wrapper
return event.data.payload ?? event.data;
}
]
} Execution: async transform | Can throw: yes
Cookie Hooks
beforeCookie
Called before a cookie is set. Return false to reject the cookie. Can also modify cookie properties.
type BeforeCookieHook = (
event: CookieEvent,
config: RezoConfig
) => boolean | void | Promise<boolean | void>; The CookieEvent provides:
cookie— the Cookie object being setsource— where it came from:'response','request', or'manual'url— URL contextisValid— whether the cookie passes validationvalidationErrors— array of validation error strings (if any)
hooks: {
beforeCookie: [
(event, config) => {
// Block third-party tracking cookies
if (event.cookie.name.startsWith('_ga') || event.cookie.name.startsWith('_fb')) {
return false;
}
}
]
} Execution: async boolean | Can throw: no (false = veto)
afterCookie
Called after cookies are processed. Use for cookie logging or analytics.
type AfterCookieHook = (
cookies: Cookie[],
config: RezoConfig
) => void | Promise<void>; hooks: {
afterCookie: [
(cookies, config) => {
for (const cookie of cookies) {
console.log(`Cookie set: ${cookie.name}=${cookie.value} (domain: ${cookie.domain})`);
}
}
]
} Execution: async void | Can throw: yes
Low-Level Event Hooks
These hooks fire during network operations and use fire-and-forget semantics. Errors are caught and logged but never propagate.
onSocket
Called on socket lifecycle events (connect, close, drain, error, timeout, end).
type OnSocketHook = (event: SocketEvent, socket: Socket | TLSSocket) => void; The SocketEvent provides:
type—'connect','close','drain','error','timeout','end'localAddress,localPort,remoteAddress,remotePort— connection endpointsbytesWritten,bytesRead— transfer statserror— error object if applicabletimestamp— event timestamp
hooks: {
onSocket: [
(event, socket) => {
if (event.type === 'connect') {
console.log(`Connected to ${event.remoteAddress}:${event.remotePort}`);
}
}
]
} Execution: sync event (fire-and-forget)
onDns
Called when DNS lookup completes.
type OnDnsHook = (event: DnsLookupEvent, config: RezoConfig) => void; The DnsLookupEvent provides:
hostname— hostname being resolvedaddress— resolved IP addressfamily— address family (4 for IPv4, 6 for IPv6)duration— DNS lookup duration in millisecondstimestamp— event timestamp
hooks: {
onDns: [
(event) => {
metrics.histogram('dns_lookup_ms', event.duration, { hostname: event.hostname });
}
]
} Execution: sync event (fire-and-forget)
onTls
Called when TLS handshake completes.
type OnTlsHook = (event: TlsHandshakeEvent, config: RezoConfig) => void; The TlsHandshakeEvent provides:
protocol— TLS version (e.g.,'TLSv1.3')cipher— cipher suite usedauthorized— whether certificate is authorizedauthorizationError— authorization error if anycertificate— server certificate info (subject, issuer, validity dates, fingerprint)duration— handshake duration in millisecondstimestamp— event timestamp
hooks: {
onTls: [
(event) => {
if (!event.authorized) {
console.warn(`TLS warning: ${event.authorizationError}`);
}
console.log(`TLS ${event.protocol} using ${event.cipher} (${event.duration}ms)`);
}
]
} Execution: sync event (fire-and-forget)
onTimeout
Called when a timeout occurs.
type OnTimeoutHook = (event: TimeoutEvent, config: RezoConfig) => void; The TimeoutEvent provides:
type—'connect','request','response','socket','lookup'timeout— configured timeout value in millisecondselapsed— elapsed time before timeout in millisecondsurl— URL being requestedtimestamp— event timestamp
hooks: {
onTimeout: [
(event) => {
alerting.send(`Timeout: ${event.type} for ${event.url} after ${event.elapsed}ms`);
}
]
} Execution: sync event (fire-and-forget)
onAbort
Called when a request is aborted.
type OnAbortHook = (event: AbortEvent, config: RezoConfig) => void; The AbortEvent provides:
reason—'user','timeout','signal','error'message— abort messageurl— URL being requestedelapsed— elapsed time before abort in millisecondstimestamp— event timestamp
hooks: {
onAbort: [
(event) => {
console.log(`Request aborted (${event.reason}): ${event.url} after ${event.elapsed}ms`);
}
]
} Execution: sync event (fire-and-forget)
Rate Limit Hook
onRateLimitWait
Called when the client is about to wait due to rate limiting. Informational only — you cannot abort the wait from this hook.
type OnRateLimitWaitHook = (
event: RateLimitWaitEvent,
config: RezoConfig
) => void | Promise<void>; The RateLimitWaitEvent provides:
status— HTTP status code that triggered the wait (e.g., 429)waitTime— time to wait in millisecondsattempt— current wait attempt number (1-indexed)maxAttempts— maximum wait attempts configuredsource— where the wait time was extracted from:'header','body','function','default'sourcePath— the header or body path used (if applicable)url,method— request detailstimestamp— event timestamp
hooks: {
onRateLimitWait: [
(event) => {
console.log(
`Rate limited (${event.status}): waiting ${event.waitTime}ms ` +
`(attempt ${event.attempt}/${event.maxAttempts}, source: ${event.source})`
);
}
]
} Execution: async void | Can throw: errors are caught in debug mode
Proxy Hooks
All proxy hooks require a proxyManager to be configured on the Rezo instance. They provide visibility into proxy selection, rotation, errors, and pool health.
beforeProxySelect
Called before a proxy is selected. Can return a specific ProxyInfo to override the selection algorithm.
type BeforeProxySelectHook = (
context: BeforeProxySelectContext
) => ProxyInfo | void | Promise<ProxyInfo | void>; hooks: {
beforeProxySelect: [
(context) => {
// Force a specific proxy for certain domains
if (context.url.includes('restricted-api.com')) {
return { protocol: 'socks5', host: '10.0.0.1', port: 1080 };
}
}
]
} Execution: async early-return
afterProxySelect
Called after a proxy is selected. Use for logging or analytics.
type AfterProxySelectHook = (context: AfterProxySelectContext) => void | Promise<void>; hooks: {
afterProxySelect: [
(context) => {
console.log(`Using proxy: ${context.proxy.host}:${context.proxy.port}`);
}
]
} Execution: async void
beforeProxyError
Called before a proxy error is processed.
type BeforeProxyErrorHook = (context: BeforeProxyErrorContext) => void | Promise<void>; Execution: async void
afterProxyError
Called after a proxy error is processed. Use for error logging or fallback logic.
type AfterProxyErrorHook = (context: AfterProxyErrorContext) => void | Promise<void>; hooks: {
afterProxyError: [
(context) => {
console.error(`Proxy ${context.proxy.host} failed: ${context.error.message}`);
}
]
} Execution: async void
beforeProxyDisable
Called before a proxy is disabled. Return false to prevent disabling.
type BeforeProxyDisableHook = (
context: BeforeProxyDisableContext
) => boolean | void | Promise<boolean | void>; hooks: {
beforeProxyDisable: [
(context) => {
// Keep premium proxies alive even after errors
if (context.proxy.host.includes('premium')) {
return false; // Prevent disabling
}
}
]
} Execution: async boolean
afterProxyDisable
Called after a proxy is disabled. Use for notifications or logging.
type AfterProxyDisableHook = (context: AfterProxyDisableContext) => void | Promise<void>; Execution: async void
afterProxyRotate
Called when proxy rotation occurs.
type AfterProxyRotateHook = (context: AfterProxyRotateContext) => void | Promise<void>; hooks: {
afterProxyRotate: [
(context) => {
console.log(`Proxy rotated: ${context.previous?.host} -> ${context.current.host}`);
}
]
} Execution: async void
afterProxyEnable
Called when a previously disabled proxy is re-enabled.
type AfterProxyEnableHook = (context: AfterProxyEnableContext) => void | Promise<void>; Execution: async void
onNoProxiesAvailable
Called when no proxies are available and an error is about to be thrown. Use for alerting, triggering external proxy pool refresh, or recording statistics about proxy pool health.
type OnNoProxiesAvailableHook = (
context: OnNoProxiesAvailableContext
) => void | Promise<void>; hooks: {
onNoProxiesAvailable: [
async (context) => {
await alerting.critical('All proxies exhausted', {
totalProxies: context.totalProxies,
disabledProxies: context.disabledProxies
});
}
]
} Execution: async void
Utility Functions
createDefaultHooks()
Creates an empty hooks object with all 26 arrays initialized. Called internally by the Rezo constructor.
import { createDefaultHooks } from 'rezo';
const hooks = createDefaultHooks();
// { init: [], beforeRequest: [], beforeRedirect: [], ... } mergeHooks(base, overrides)
Merges two hooks objects by concatenating arrays. Base hooks run first, then overrides are appended.
import { createDefaultHooks, mergeHooks } from 'rezo';
const base = createDefaultHooks();
base.beforeRequest.push(loggingHook);
const merged = mergeHooks(base, {
beforeRequest: [authHook],
afterResponse: [transformHook]
});
// beforeRequest: [loggingHook, authHook]
// afterResponse: [transformHook] This is exactly what the Rezo constructor does — it creates default hooks and merges any hooks from your config:
// Internal: this.hooks = mergeHooks(createDefaultHooks(), config?.hooks); Hooks at Instance vs Request Level
Hooks set on the Rezo instance apply to all requests:
const client = new Rezo({
hooks: {
beforeRequest: [globalLogger],
afterResponse: [globalTransform]
}
}); The hooks live on client.hooks and can be modified at any time:
// Add a hook after construction
client.hooks.onDns.push((event) => {
dnsMetrics.record(event);
}); For per-request hooks, pass them through the hooks on the config and the hook system’s merging behavior ensures both instance-level and request-level hooks execute.