CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ofetch

Universal HTTP fetch library with intelligent parsing, error handling, retry logic, and cross-environment compatibility

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

interceptors.mddocs/

Request and Response Interceptors

Lifecycle hooks for modifying requests, handling responses, implementing logging, authentication, and custom logic during the fetch process.

Capabilities

Interceptor System Overview

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[];

Request Interceptor

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);
    }
  ]
});

Request Error Interceptor

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
    });
  }
});

Response Interceptor

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");
    }
  }
});

Response Error Interceptor

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;
    }
  }
});

Hook Chaining and Arrays

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");
  }
});

Advanced Hook Patterns

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

docs

configuration.md

core-operations.md

error-handling.md

index.md

interceptors.md

utilities.md

tile.json