Documentation Index
Fetch the complete documentation index at: https://mintlify.com/unjs/ofetch/llms.txt
Use this file to discover all available pages before exploring further.
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