Universal HTTP fetch library with intelligent parsing, error handling, retry logic, and cross-environment compatibility
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Lifecycle hooks for modifying requests, handling responses, implementing logging, authentication, and custom logic during the fetch process.
ofetch provides four lifecycle hooks that allow you to intercept and modify requests and responses at different stages of the fetch process.
interface FetchHooks<T = any, R extends ResponseType = ResponseType> {
onRequest?: MaybeArray<FetchHook<FetchContext<T, R>>>;
onRequestError?: MaybeArray<FetchHook<FetchContext<T, R> & { error: Error }>>;
onResponse?: MaybeArray<FetchHook<FetchContext<T, R> & { response: FetchResponse<T> }>>;
onResponseError?: MaybeArray<FetchHook<FetchContext<T, R> & { response: FetchResponse<T> }>>;
}
type FetchHook<C extends FetchContext = FetchContext> = (
context: C
) => MaybePromise<void>;
type MaybePromise<T> = T | Promise<T>;
type MaybeArray<T> = T | T[];Called before the request is sent, allowing modification of options, URL, headers, and logging.
/**
* Hook called before request is sent
* @param context - Contains request and options that can be modified
*/
onRequest?: MaybeArray<FetchHook<FetchContext<T, R>>>;
interface FetchContext<T = any, R extends ResponseType = ResponseType> {
request: FetchRequest;
options: ResolvedFetchOptions<R>;
response?: FetchResponse<T>;
error?: Error;
}Usage Examples:
import { ofetch } from "ofetch";
// Add authentication token
const api = ofetch.create({
baseURL: "https://api.example.com",
async onRequest({ request, options }) {
// Add auth token to all requests
const token = await getAuthToken();
options.headers.set("Authorization", `Bearer ${token}`);
}
});
// Request logging
const api = ofetch.create({
onRequest({ request, options }) {
console.log(`[${options.method || "GET"}] ${request}`);
console.log("Headers:", Object.fromEntries(options.headers));
}
});
// Add timestamp to query parameters
const api = ofetch.create({
onRequest({ request, options }) {
options.query = options.query || {};
options.query.timestamp = Date.now();
}
});
// Multiple request hooks
const data = await ofetch("/api/data", {
onRequest: [
({ options }) => {
options.headers.set("X-Client", "ofetch");
},
async ({ options }) => {
const userId = await getCurrentUserId();
options.headers.set("X-User-ID", userId);
}
]
});Called when the fetch request fails (network error, timeout, etc.), before retry logic is applied.
/**
* Hook called when fetch request encounters an error
* @param context - Contains request, options, and error
*/
onRequestError?: MaybeArray<FetchHook<FetchContext<T, R> & { error: Error }>>;Usage Examples:
import { ofetch } from "ofetch";
// Log request errors
const api = ofetch.create({
onRequestError({ request, error }) {
console.error(`Request failed: ${request}`, error.message);
}
});
// Custom error handling
const api = ofetch.create({
onRequestError({ request, options, error }) {
if (error.name === "TimeoutError") {
console.log(`Request to ${request} timed out after ${options.timeout}ms`);
} else if (error.name === "AbortError") {
console.log(`Request to ${request} was aborted`);
}
}
});
// Error metrics collection
const api = ofetch.create({
onRequestError({ request, error }) {
metrics.increment("http.request.error", {
url: String(request),
error_type: error.name
});
}
});Called after a successful response is received and parsed, allowing response modification and logging.
/**
* Hook called after successful response is received and parsed
* @param context - Contains request, options, and response with parsed data
*/
onResponse?: MaybeArray<FetchHook<FetchContext<T, R> & { response: FetchResponse<T> }>>;
interface FetchResponse<T> extends Response {
_data?: T;
}Usage Examples:
import { ofetch } from "ofetch";
// Response logging
const api = ofetch.create({
onResponse({ request, response }) {
console.log(`[${response.status}] ${request}`);
console.log("Response time:", response.headers.get("x-response-time"));
}
});
// Response caching
const cache = new Map();
const api = ofetch.create({
onResponse({ request, response }) {
if (response.status === 200) {
cache.set(String(request), response._data);
}
}
});
// Response transformation
const api = ofetch.create({
onResponse({ response }) {
// Transform all responses to include metadata
if (response._data && typeof response._data === "object") {
response._data.meta = {
status: response.status,
timestamp: Date.now()
};
}
}
});
// Response validation
const api = ofetch.create({
onResponse({ response }) {
if (response._data && !isValidResponse(response._data)) {
throw new Error("Invalid response format");
}
}
});Called when a response has an error status (4xx, 5xx) but fetch succeeded, before retry logic is applied.
/**
* Hook called when response has error status (4xx, 5xx)
* @param context - Contains request, options, and error response
*/
onResponseError?: MaybeArray<FetchHook<FetchContext<T, R> & { response: FetchResponse<T> }>>;Usage Examples:
import { ofetch } from "ofetch";
// Handle authentication errors
const api = ofetch.create({
async onResponseError({ response }) {
if (response.status === 401) {
await refreshAuthToken();
// Note: This won't retry the request automatically
// You'd need to handle retry manually or let default retry logic handle it
}
}
});
// Log error responses
const api = ofetch.create({
onResponseError({ request, response }) {
console.error(`[${response.status}] ${request}:`, response._data);
}
});
// Custom error handling by status
const api = ofetch.create({
onResponseError({ response }) {
switch (response.status) {
case 400:
console.error("Bad request:", response._data);
break;
case 403:
console.error("Forbidden - check permissions");
break;
case 429:
console.warn("Rate limited - request will be retried");
break;
case 500:
console.error("Server error:", response._data);
break;
}
}
});Multiple hooks can be provided as arrays and will be executed sequentially.
Usage Examples:
import { ofetch } from "ofetch";
// Multiple hooks as array
const api = ofetch.create({
onRequest: [
({ options }) => {
options.headers.set("X-Client", "ofetch");
},
async ({ options }) => {
const token = await getToken();
options.headers.set("Authorization", `Bearer ${token}`);
},
({ request, options }) => {
console.log(`[${options.method || "GET"}] ${request}`);
}
],
onResponse: [
({ response }) => {
console.log(`Response: ${response.status}`);
},
({ response }) => {
metrics.recordResponseTime(response.headers.get("x-response-time"));
}
]
});
// Per-request hooks combined with instance hooks
const data = await api("/api/data", {
onRequest({ options }) {
options.headers.set("X-Custom", "per-request");
},
onResponse({ response }) {
console.log("Per-request response handler");
}
});Complex interceptor patterns for authentication, caching, and error handling.
Usage Examples:
import { ofetch } from "ofetch";
// Automatic token refresh with retry
const api = ofetch.create({
baseURL: "https://api.example.com",
async onRequest({ options }) {
const token = await getValidToken(); // Refreshes if expired
options.headers.set("Authorization", `Bearer ${token}`);
},
async onResponseError({ response, request, options }) {
if (response.status === 401) {
await clearTokenCache();
// Let retry logic handle the retry with fresh token
}
}
});
// Request/response correlation
const correlationMap = new Map();
const api = ofetch.create({
onRequest({ request, options }) {
const id = Math.random().toString(36);
options.headers.set("X-Correlation-ID", id);
correlationMap.set(id, { start: Date.now(), request });
},
onResponse({ response }) {
const id = response.headers.get("X-Correlation-ID");
if (id && correlationMap.has(id)) {
const { start, request } = correlationMap.get(id);
console.log(`${request} completed in ${Date.now() - start}ms`);
correlationMap.delete(id);
}
}
});
// Conditional interceptors
const api = ofetch.create({
onRequest({ request, options }) {
// Only add analytics for specific endpoints
if (String(request).includes("/api/analytics")) {
options.headers.set("X-Analytics", "true");
}
},
onResponse({ response, request }) {
// Only cache GET requests with 2xx status
if (options.method === "GET" && response.ok) {
cacheResponse(String(request), response._data);
}
}
});Install with Tessl CLI
npx tessl i tessl/npm-ofetch