Guide
Hooks
Intercept and customize request/response lifecycle with hooks.
Hooks provide a powerful way to intercept and customize the request/response lifecycle. You can use hooks for logging, authentication, error handling, request transformation, and more.
Available Hooks
The library provides four lifecycle hooks:
init
Hook<[config: RequestConfig], void>
Runs on request initialization, before any other hook. Called only once even with retries.
Perfect for initial setup, logging, or request ID generation.
Perfect for initial setup, logging, or request ID generation.
request
Hook<[config: RequestConfig, state: RequestState], void>
Runs before each request is sent. Called for each retry attempt.
Ideal for adding/modifying headers, logging attempts, or request transformation.
Ideal for adding/modifying headers, logging attempts, or request transformation.
response
Hook<[response: Response], void>
Runs after a successful response is received.
Useful for response logging, caching, or data transformation.
Useful for response logging, caching, or data transformation.
error
Hook<[error: RequestError], Error | void>
Runs when a request error occurs.
Can return a new
Can return a new
Error to replace the original error, or void to keep it.Execution Order
- init - Called once when request starts
- request - Called before each attempt (including retries)
- Request is sent
- Either:
- response - On success
- error - On failure
Registering Hooks
Use the on method to register hooks:
import { createClient } from '@outloud/reqo'
const client = createClient({
url: 'https://api.example.com'
})
// Register a hook
client.on('request', (config) => {
console.log(`Requesting: ${config.method} ${config.url}`)
})
// Register multiple hooks of the same type
client.on('request', (config) => {
config.headers.set('X-Request-Time', Date.now().toString())
})
client.on('response', (response) => {
console.log(`Response: ${response.status}`)
})
Removing Hooks
Use the off method to unregister hooks:
const requestLogger = (config) => {
console.log(`Request: ${config.method} ${config.url}`)
}
// Register
client.on('request', requestLogger)
// Unregister
client.off('request', requestLogger)
Examples
init
Called once when a request is initialized:
client.on('init', (config) => {
// Generate unique request ID
config.headers.set('X-Request-ID', crypto.randomUUID())
// Log request initiation
console.log('Initializing request:', config.url)
})
request
Called before each request (including retries):
client.on('request', (config, state) => {
// Add retry count to headers
config.headers.set('X-Retry-Attempt', state.retryCount.toString())
// Refresh authentication token (override existing header on retry)
if (shouldRefreshToken()) {
config.headers.set('Authorization', `Bearer ${getNewToken()}`, true)
}
console.log(`Attempt ${state.retryCount}: ${config.method} ${config.url}`)
// Check if this is a retry
if (state.error) {
console.log(`Retrying after error: ${state.error.message}`)
}
})
response
Called after successful responses:
client.on('response', (response) => {
// Log response details
console.log('Response received:', {
status: response.status,
contentType: response.headers.get('content-type'),
size: response.headers.get('content-length')
})
// Cache response
if (response.ok) {
cache.set(response.url, response.data)
}
// Track rate limiting
const remaining = response.headers.get('X-RateLimit-Remaining')
if (remaining) {
console.log(`Rate limit remaining: ${remaining}`)
}
})
error
Transform or handle errors:
client.on('error', (error) => {
// Log error details
console.error('Request failed:', {
url: error.url,
method: error.method,
status: error.status,
message: error.message
})
// Transform specific errors
if (error.status === 401) {
return new Error('Authentication failed. Please log in again.')
}
if (error.status === 429) {
return new Error('Rate limit exceeded. Please try again later.')
}
// Return void to keep original error
})
Async Hooks
Hooks can be asynchronous:
client.on('request', async (config) => {
// Fetch fresh token
const token = await getAuthToken()
config.headers.set('Authorization', `Bearer ${token}`)
})
client.on('response', async (response) => {
// Store in database
await db.insert('requests', {
url: response.url,
status: response.status,
timestamp: Date.now()
})
})
Practical Examples
Authentication Refresh
let accessToken = 'initial-token'
client.on('request', async (config, state) => {
// Check if token needs refresh
if (isTokenExpired(accessToken) || state.error?.status === 401) {
accessToken = await refreshAccessToken()
}
config.headers.set('Authorization', `Bearer ${accessToken}`)
})
Request/Response Logging
client.on('init', (config) => {
console.log(`[${config.id}] Initialized: ${config.method} ${config.url}`)
})
client.on('request', (config) => {
console.log(`[${config.id}] Sending request...`)
})
client.on('response', (response) => {
console.log(`[${response.url}] Received: ${response.status}`)
})
client.on('error', (error) => {
console.error(`[${error.client}] Failed: ${error.status} ${error.message}`)
})
Response Caching
const cache = new Map()
client.on('init', (config) => {
// Check cache before making request
const cacheKey = `${config.method}:${config.url}`
if (cache.has(cacheKey)) {
// Note: Hooks can't prevent request execution
// You'd need to implement caching at a higher level
console.log('Cache hit:', cacheKey)
}
})
client.on('response', (response) => {
// Cache successful GET responses
if (response.ok && response.status === 200) {
const cacheKey = `GET:${response.url}`
cache.set(cacheKey, {
data: response.data,
timestamp: Date.now()
})
}
})
Error Monitoring
client.on('error', (error) => {
// Send to error tracking service
if (error.status >= 500) {
errorTracker.captureException(error, {
tags: {
url: error.url,
method: error.method,
status: error.status
},
extra: {
params: error.params,
data: error.data
}
})
}
})
Request Timing
const timings = new Map()
client.on('init', (config) => {
timings.set(config.url, Date.now())
})
client.on('response', (response) => {
const startTime = timings.get(response.url)
if (startTime) {
const duration = Date.now() - startTime
console.log(`Request to ${response.url} took ${duration}ms`)
timings.delete(response.url)
}
})
client.on('error', (error) => {
timings.delete(error.url)
})