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.
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.
response
Hook<[response: Response], void>
Runs after a successful response is received.

Useful for response logging, caching, or data transformation.
error
Hook<[error: RequestError], Error | void>
Runs when a request error occurs.

Can return a new Error to replace the original error, or void to keep it.

Execution Order

  1. init - Called once when request starts
  2. request - Called before each attempt (including retries)
  3. Request is sent
  4. 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)
})