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')
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
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:
onRequest hooks (if request is valid)
- Fetch request is made
onRequestError (if request fails) → throws error
onResponse (if status < 400)
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