Skip to main content
ofetch provides powerful lifecycle hooks (interceptors) that allow you to intercept and modify requests and responses at different stages.

Available Hooks

There are four lifecycle hooks available:
  • onRequest - Called before the request is sent
  • onRequestError - Called when the request fails (network error, invalid URL, etc.)
  • onResponse - Called after a successful response is received
  • onResponseError - Called when the response status is 4xx or 5xx

Hook Signatures

All hooks receive a FetchContext object with the following properties:
interface FetchContext<T = any> {
  request: FetchRequest          // URL string or Request object
  options: FetchOptions          // Resolved request options
  response?: FetchResponse<T>    // Response object (in onResponse/onResponseError)
  error?: Error                  // Error object (in onRequestError)
}

onRequest

Called before the request is sent. Use this to modify headers, add authentication, log requests, etc.
onRequest
(context: FetchContext) => void | Promise<void>
Hook called before the request is sent. Can be a single function or an array of functions.

Adding Authentication

import { ofetch } from 'ofetch'

const api = ofetch.create({
  baseURL: 'https://api.example.com',
  onRequest: ({ options }) => {
    // Add authorization header
    const token = localStorage.getItem('token')
    if (token) {
      options.headers.set('Authorization', `Bearer ${token}`)
    }
  }
})

await api('/user/profile')

Logging Requests

import { ofetch } from 'ofetch'

const data = await ofetch('/api/data', {
  onRequest: ({ request, options }) => {
    console.log(`Making ${options.method} request to:`, request)
    console.log('Headers:', options.headers)
    console.log('Body:', options.body)
  }
})

Modifying Request Body

import { ofetch } from 'ofetch'

const result = await ofetch('/api/users', {
  method: 'POST',
  body: { name: 'John' },
  onRequest: ({ options }) => {
    // Add timestamp to all POST requests
    if (options.method === 'POST' && options.body) {
      options.body = {
        ...options.body,
        timestamp: new Date().toISOString()
      }
    }
  }
})

Multiple Request Hooks

import { ofetch } from 'ofetch'

const data = await ofetch('/api/data', {
  onRequest: [
    ({ options }) => {
      // First hook: Add auth
      options.headers.set('Authorization', 'Bearer token')
    },
    ({ options }) => {
      // Second hook: Add tracking ID
      options.headers.set('X-Request-ID', crypto.randomUUID())
    },
    ({ request }) => {
      // Third hook: Log
      console.log('Sending request to:', request)
    }
  ]
})

onRequestError

Called when the request fails before receiving a response (network errors, invalid URLs, etc.).
onRequestError
(context: FetchContext & { error: Error }) => void | Promise<void>
Hook called when the request fails. The context includes an error property with the error details.
import { ofetch } from 'ofetch'

try {
  await ofetch('https://invalid-domain-that-does-not-exist.com', {
    onRequestError: ({ request, error }) => {
      console.error(`Request to ${request} failed:`, error.message)
      
      // Log to error tracking service
      errorTracker.log({
        type: 'network_error',
        url: request,
        error: error.message
      })
    }
  })
} catch (error) {
  // Error is still thrown after onRequestError is called
  console.error('Caught error:', error)
}

onResponse

Called after a successful response is received (status < 400). Use this to transform responses, cache data, or log responses.
onResponse
(context: FetchContext & { response: FetchResponse }) => void | Promise<void>
Hook called after receiving a successful response. The context includes a response property with the Response object.

Logging Responses

import { ofetch } from 'ofetch'

const data = await ofetch('/api/data', {
  onResponse: ({ response }) => {
    console.log('Response status:', response.status)
    console.log('Response headers:', response.headers)
    console.log('Response data:', response._data)
  }
})

Caching Responses

import { ofetch } from 'ofetch'

const cache = new Map()

const api = ofetch.create({
  onResponse: ({ request, response }) => {
    // Cache GET requests
    if (response.status === 200) {
      cache.set(request.toString(), response._data)
    }
  }
})

await api('/api/user/123')

Transforming Response Data

import { ofetch } from 'ofetch'

const data = await ofetch('/api/dates', {
  onResponse: ({ response }) => {
    // Transform ISO date strings to Date objects
    if (response._data?.createdAt) {
      response._data.createdAt = new Date(response._data.createdAt)
    }
    if (response._data?.updatedAt) {
      response._data.updatedAt = new Date(response._data.updatedAt)
    }
  }
})

console.log(data.createdAt instanceof Date) // true

Performance Monitoring

import { ofetch } from 'ofetch'

const data = await ofetch('/api/data', {
  async onRequest: ({ request }) => {
    // Store start time
    request._startTime = Date.now()
  },
  async onResponse: ({ request, response }) => {
    const duration = Date.now() - request._startTime
    console.log(`Request took ${duration}ms`)
    
    // Send to analytics
    analytics.track('api_request', {
      url: request,
      duration,
      status: response.status
    })
  }
})

onResponseError

Called when the response status is 4xx or 5xx. Use this to handle errors globally, refresh tokens, or log errors.
onResponseError
(context: FetchContext & { response: FetchResponse }) => void | Promise<void>
Hook called when the response status indicates an error (400-599). The context includes a response property with the Response object.

Global Error Handling

import { ofetch } from 'ofetch'

const api = ofetch.create({
  baseURL: 'https://api.example.com',
  onResponseError: ({ response }) => {
    // Show toast notification for errors
    if (response.status === 401) {
      showToast('Please log in again')
    } else if (response.status === 403) {
      showToast('You do not have permission')
    } else if (response.status >= 500) {
      showToast('Server error, please try again later')
    }
  }
})

Token Refresh on 401

import { ofetch } from 'ofetch'

const api = ofetch.create({
  baseURL: 'https://api.example.com',
  async onResponseError({ response, options }) {
    if (response.status === 401) {
      // Try to refresh token
      const refreshToken = localStorage.getItem('refreshToken')
      if (refreshToken) {
        try {
          const { token } = await ofetch('/auth/refresh', {
            method: 'POST',
            body: { refreshToken }
          })
          
          // Store new token
          localStorage.setItem('token', token)
          
          // Retry original request with new token
          options.headers.set('Authorization', `Bearer ${token}`)
        } catch (error) {
          // Refresh failed, redirect to login
          window.location.href = '/login'
        }
      } else {
        // No refresh token, redirect to login
        window.location.href = '/login'
      }
    }
  }
})

Error Logging

import { ofetch } from 'ofetch'

const data = await ofetch('/api/data', {
  onResponseError: ({ request, response }) => {
    // Log error to tracking service
    errorTracker.log({
      type: 'api_error',
      url: request,
      status: response.status,
      statusText: response.statusText,
      data: response._data
    })
  }
})

Hook Execution Order

Hooks are executed in this order:
  1. onRequest hooks (if request is valid)
  2. Fetch request is made
  3. onRequestError (if request fails) → throws error
  4. onResponse (if status < 400)
  5. onResponseError (if status >= 400) → throws error
import { ofetch } from 'ofetch'

await ofetch('/api/data', {
  onRequest: () => console.log('1. onRequest'),
  onRequestError: () => console.log('2. onRequestError (only if network error)'),
  onResponse: () => console.log('3. onResponse (only if status < 400)'),
  onResponseError: () => console.log('4. onResponseError (only if status >= 400)')
})

Hook Error Handling

If a hook throws an error, the request is aborted and the error is thrown:
import { ofetch } from 'ofetch'

try {
  await ofetch('/api/data', {
    onRequest: () => {
      throw new Error('Invalid request')
    }
  })
} catch (error) {
  console.error(error.message) // "Invalid request"
}
If you throw an error in a hook, the request will be aborted and the error will be propagated to the caller.

Complete Example: API Client with Interceptors

import { $fetch } from 'ofetch'

// Create a configured API client
const api = $fetch.create({
  baseURL: 'https://api.example.com',
  
  // Add auth token to all requests
  onRequest: ({ options }) => {
    const token = localStorage.getItem('token')
    if (token) {
      options.headers.set('Authorization', `Bearer ${token}`)
    }
    
    // Add request ID for tracking
    options.headers.set('X-Request-ID', crypto.randomUUID())
    
    // Log request
    console.log(`[${options.method}] ${options.url}`)
  },
  
  // Handle request errors (network issues)
  onRequestError: ({ error }) => {
    console.error('Network error:', error.message)
    showToast('Network error, please check your connection')
  },
  
  // Process successful responses
  onResponse: ({ response }) => {
    console.log(`Response: ${response.status}`)
    
    // Parse dates
    if (response._data) {
      transformDates(response._data)
    }
  },
  
  // Handle API errors
  async onResponseError({ response, options }) {
    if (response.status === 401) {
      // Try to refresh token
      const newToken = await refreshToken()
      if (newToken) {
        options.headers.set('Authorization', `Bearer ${newToken}`)
      } else {
        window.location.href = '/login'
      }
    } else if (response.status === 403) {
      showToast('You do not have permission to access this resource')
    } else if (response.status >= 500) {
      showToast('Server error, please try again later')
      
      // Log to error tracking
      errorTracker.log({
        url: response.url,
        status: response.status,
        data: response._data
      })
    }
  }
})

// Helper functions
function transformDates(obj: any) {
  for (const key in obj) {
    if (typeof obj[key] === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(obj[key])) {
      obj[key] = new Date(obj[key])
    } else if (typeof obj[key] === 'object') {
      transformDates(obj[key])
    }
  }
}

async function refreshToken() {
  const refreshToken = localStorage.getItem('refreshToken')
  if (!refreshToken) return null
  
  try {
    const { token } = await $fetch('/auth/refresh', {
      method: 'POST',
      body: { refreshToken }
    })
    localStorage.setItem('token', token)
    return token
  } catch {
    return null
  }
}

function showToast(message: string) {
  console.log('Toast:', message)
}

const errorTracker = {
  log: (data: any) => console.error('Error:', data)
}

// Usage
export default api

Build docs developers (and LLMs) love