or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

errors.mdhooks.mdhttp-methods.mdindex.mdinstances.mdoptions.mdpagination.mdresponses.mdstreams.mdutilities.md
tile.json

hooks.mddocs/

Hooks System

Request lifecycle hooks for customizing behavior at different stages of the request process, enabling middleware-like functionality and advanced request/response processing.

Capabilities

Hooks Interface

The main hooks interface defining all available hook types and their execution points.

/**
 * Request lifecycle hooks configuration
 */
interface Hooks {
  /**
   * Called with plain request options before normalization
   * @default []
   */
  init?: InitHook[];
  
  /**
   * Called before the request is made
   * @default []
   */
  beforeRequest?: BeforeRequestHook[];
  
  /**
   * Called before following a redirect
   * @default []
   */
  beforeRedirect?: BeforeRedirectHook[];
  
  /**
   * Called before throwing an error
   * @default []
   */
  beforeError?: BeforeErrorHook[];
  
  /**
   * Called before retrying a request
   * @default []
   */
  beforeRetry?: BeforeRetryHook[];
  
  /**
   * Called after receiving a response
   * @default []
   */
  afterResponse?: AfterResponseHook[];
}

Init Hook

Called with plain request options before they are normalized and validated.

/**
 * Hook called during options initialization
 * @param init - Raw input options
 * @param self - Current Options instance
 */
type InitHook = (init: OptionsInit, self: Options) => void;

Usage Examples:

import got from "got";

const api = got.extend({
  hooks: {
    init: [
      (init, self) => {
        // Add default headers
        init.headers = {
          "User-Agent": "MyApp/1.0",
          ...init.headers
        };
        
        // Set default timeout if not specified
        if (!init.timeout) {
          init.timeout = { request: 10000 };
        }
        
        console.log("Initializing request to:", init.url);
      }
    ]
  }
});

// Init hook will be called for each request
await api.get("https://api.example.com/users");

Before Request Hook

Called before the HTTP request is made, allowing modification of final options.

/**
 * Hook called before making the HTTP request
 * @param options - Final normalized options
 * @returns Promise or void, or Response to short-circuit
 */
type BeforeRequestHook = (options: Options) => Promisable<void | Response | ResponseLike>;

type Promisable<T> = T | Promise<T>;

Usage Examples:

import got from "got";

const api = got.extend({
  hooks: {
    beforeRequest: [
      async (options) => {
        // Add authentication token
        const token = await getAuthToken();
        options.headers.Authorization = `Bearer ${token}`;
        
        // Log request details
        console.log(`→ ${options.method} ${options.url}`);
        console.log("Headers:", options.headers);
        
        // Validate required headers
        if (!options.headers.Authorization) {
          throw new Error("Authentication required");
        }
      },
      
      (options) => {
        // Add request timestamp
        options.headers["X-Request-Time"] = new Date().toISOString();
        
        // Modify URL based on environment
        if (process.env.NODE_ENV === "development") {
          options.url = options.url.replace("api.example.com", "dev-api.example.com");
        }
      }
    ]
  }
});

// Short-circuit response
const cachedApi = got.extend({
  hooks: {
    beforeRequest: [
      async (options) => {
        // Check cache before making request
        const cached = await getFromCache(options.url.toString());
        if (cached) {
          // Return cached response without making HTTP request
          return {
            statusCode: 200,
            body: cached.data,
            headers: cached.headers,
            requestUrl: options.url,
            ok: true
          } as Response;
        }
      }
    ]
  }
});

Before Redirect Hook

Called before following HTTP redirects, allowing modification of redirect options.

/**
 * Hook called before following a redirect
 * @param updatedOptions - Options for the redirect request  
 * @param plainResponse - Original response that triggered redirect
 * @returns Promise or void
 */
type BeforeRedirectHook = (updatedOptions: Options, plainResponse: PlainResponse) => Promisable<void>;

Usage Examples:

import got from "got";

const api = got.extend({
  hooks: {
    beforeRedirect: [
      (options, response) => {
        console.log(`Redirecting from ${response.requestUrl} to ${options.url}`);
        console.log(`Redirect status: ${response.statusCode}`);
        
        // Remove authorization header on external redirects
        const originalHost = response.requestUrl.hostname;
        const redirectHost = options.url.hostname;
        
        if (originalHost !== redirectHost) {
          console.log("External redirect detected, removing auth header");
          delete options.headers.Authorization;
        }
        
        // Limit redirect chains
        if (response.redirectUrls.length > 5) {
          throw new Error("Too many redirects");
        }
      }
    ]
  }
});

const response = await api.get("https://example.com/redirect-chain");

Before Error Hook

Called before throwing request errors, allowing error modification or recovery.

/**
 * Hook called before throwing an error
 * @param error - The error that would be thrown
 * @returns Promise resolving to modified error or new error
 */
type BeforeErrorHook = (error: RequestError) => Promisable<RequestError>;

Usage Examples:

import got, { HTTPError, RequestError } from "got";

const api = got.extend({
  hooks: {
    beforeError: [
      async (error) => {
        // Add context to errors
        error.message = `[${new Date().toISOString()}] ${error.message}`;
        
        // Log errors
        console.error("Request failed:", {
          url: error.options?.url,
          method: error.options?.method,
          code: error.code,
          message: error.message
        });
        
        // Transform specific errors
        if (error instanceof HTTPError && error.response.statusCode === 401) {
          // Try to refresh token on 401
          try {
            await refreshAuthToken();
            // Create new error with helpful message
            const newError = new Error("Authentication expired, please retry");
            newError.name = "AuthenticationExpiredError";
            return newError as RequestError;
          } catch {
            // If refresh fails, return original error
            return error;
          }
        }
        
        // Add retry suggestion for network errors
        if (["ENOTFOUND", "ECONNREFUSED", "ETIMEDOUT"].includes(error.code)) {
          error.message += " (Consider retrying the request)";
        }
        
        return error;
      }
    ]
  }
});

try {
  await api.get("https://api.example.com/protected");
} catch (error) {
  console.log("Enhanced error:", error.message);
}

Before Retry Hook

Called before retrying failed requests, allowing retry customization.

/**
 * Hook called before retrying a request
 * @param error - The error that triggered the retry
 * @param retryCount - Current retry attempt number
 * @returns Promise or void
 */
type BeforeRetryHook = (error: RequestError, retryCount: number) => Promisable<void>;

Usage Examples:

import got, { HTTPError } from "got";

const api = got.extend({
  retry: {
    limit: 3,
    methods: ["GET", "POST", "PUT", "DELETE"]
  },
  hooks: {
    beforeRetry: [
      async (error, retryCount) => {
        console.log(`Retry attempt ${retryCount} for ${error.options?.url}`);
        console.log(`Error: ${error.code} - ${error.message}`);
        
        // Exponential backoff with jitter
        const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
        const jitter = Math.random() * 1000;
        
        console.log(`Waiting ${delay + jitter}ms before retry...`);
        await new Promise(resolve => setTimeout(resolve, delay + jitter));
        
        // Refresh auth token before retry on 401 errors
        if (error instanceof HTTPError && error.response.statusCode === 401) {
          console.log("Refreshing authentication token...");
          await refreshAuthToken();
          
          // Update authorization header in options
          const newToken = await getAuthToken();
          error.options.headers.Authorization = `Bearer ${newToken}`;
        }
        
        // Clear cache on server errors
        if (error instanceof HTTPError && error.response.statusCode >= 500) {
          console.log("Server error detected, clearing cache");
          await clearRequestCache();
        }
      }
    ]
  }
});

const response = await api.get("https://unreliable-api.example.com");

After Response Hook

Called after receiving responses, allowing response modification or additional processing.

/**
 * Hook called after receiving a response
 * @param response - The HTTP response
 * @param retryWithMergedOptions - Function to retry with new options
 * @returns Promise resolving to response or new response
 */
type AfterResponseHook<ResponseType = unknown> = (
  response: Response<ResponseType>,
  retryWithMergedOptions: (options: OptionsInit) => never
) => Promisable<Response | CancelableRequest<Response>>;

Usage Examples:

import got from "got";

const api = got.extend({
  hooks: {
    afterResponse: [
      async (response, retryWithMergedOptions) => {
        console.log(`← ${response.statusCode} ${response.requestUrl}`);
        console.log(`Response time: ${response.timings.phases.total}ms`);
        
        // Cache successful responses
        if (response.ok && response.request.options.method === "GET") {
          await cacheResponse(response.requestUrl.toString(), {
            data: response.body,
            headers: response.headers,
            timestamp: Date.now()
          });
        }
        
        // Handle rate limiting with retry
        if (response.statusCode === 429) {
          const retryAfter = response.headers["retry-after"];
          if (retryAfter) {
            const delay = parseInt(retryAfter as string) * 1000;
            console.log(`Rate limited, retrying after ${delay}ms`);
            
            await new Promise(resolve => setTimeout(resolve, delay));
            
            // Retry the request (this will throw and restart the request)
            retryWithMergedOptions({});
          }
        }
        
        // Transform response data
        if (response.headers["content-type"]?.includes("application/json")) {
          try {
            const data = typeof response.body === "string" 
              ? JSON.parse(response.body) 
              : response.body;
            
            // Add metadata to response
            const enhanced = {
              ...data,
              _metadata: {
                requestTime: response.timings.phases.total,
                fromCache: response.isFromCache,
                retryCount: response.retryCount
              }
            };
            
            // Return modified response
            return {
              ...response,
              body: enhanced
            };
          } catch {
            // Return original response if JSON parsing fails
            return response;
          }
        }
        
        return response;
      }
    ]
  }
});

const response = await api.get("https://api.example.com/data");
console.log(response.body._metadata); // Added by hook

Hook Composition and Ordering

Hooks execute in the order they are defined and can be composed across instance extensions.

import got from "got";

// Base hooks
const baseApi = got.extend({
  hooks: {
    beforeRequest: [
      (options) => {
        console.log("Base: Adding User-Agent");
        options.headers["User-Agent"] = "MyApp/1.0";
      }
    ],
    afterResponse: [
      (response) => {
        console.log("Base: Logging response");
        console.log(`Response: ${response.statusCode}`);
        return response;
      }
    ]
  }
});

// Extended hooks
const enhancedApi = baseApi.extend({
  hooks: {
    beforeRequest: [
      async (options) => {
        console.log("Enhanced: Adding auth token");
        const token = await getAuthToken();
        options.headers.Authorization = `Bearer ${token}`;
      }
    ],
    afterResponse: [
      async (response) => {
        console.log("Enhanced: Caching response");
        await cacheResponse(response.requestUrl.toString(), response.body);
        return response;
      }
    ]
  }
});

// Execution order:
// 1. Base beforeRequest hook (User-Agent)
// 2. Enhanced beforeRequest hook (Authorization)  
// 3. HTTP request is made
// 4. Base afterResponse hook (logging)
// 5. Enhanced afterResponse hook (caching)

await enhancedApi.get("https://api.example.com/data");

Advanced Hook Patterns

Middleware-style hook composition:

import got from "got";

// Create middleware functions
const addAuth = (token: string) => ({
  beforeRequest: [(options: Options) => {
    options.headers.Authorization = `Bearer ${token}`;
  }]
});

const addLogging = () => ({
  beforeRequest: [(options: Options) => {
    console.log(`→ ${options.method} ${options.url}`);
  }],
  afterResponse: [(response: Response) => {
    console.log(`← ${response.statusCode}`);
    return response;
  }]
});

const addRetryDelay = (baseDelay: number = 1000) => ({
  beforeRetry: [async (error: RequestError, retryCount: number) => {
    const delay = baseDelay * Math.pow(2, retryCount - 1);
    await new Promise(resolve => setTimeout(resolve, delay));
  }]
});

// Compose middleware
const api = got.extend({
  hooks: {
    ...addAuth("your-token"),
    ...addLogging(),
    ...addRetryDelay(500)
  }
});

Request/Response transformation pipeline:

import got from "got";

const api = got.extend({
  hooks: {
    beforeRequest: [
      // Transform request data
      (options) => {
        if (options.json && typeof options.json === "object") {
          // Convert camelCase to snake_case for API
          options.json = transformKeys(options.json, camelToSnake);
        }
      }
    ],
    afterResponse: [
      // Transform response data
      (response) => {
        if (response.body && typeof response.body === "object") {
          // Convert snake_case to camelCase for client
          const transformed = transformKeys(response.body, snakeToCamel);
          return {
            ...response,
            body: transformed
          };
        }
        return response;
      }
    ]
  }
});

// Helper functions
function transformKeys(obj: any, transformer: (key: string) => string): any {
  if (Array.isArray(obj)) {
    return obj.map(item => transformKeys(item, transformer));
  }
  if (obj && typeof obj === "object") {
    return Object.keys(obj).reduce((result, key) => {
      const newKey = transformer(key);
      result[newKey] = transformKeys(obj[key], transformer);
      return result;
    }, {} as any);
  }
  return obj;
}

const camelToSnake = (str: string) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
const snakeToCamel = (str: string) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());

Error Handling in Hooks

Proper error handling within hooks to avoid breaking request flow.

import got from "got";

const api = got.extend({
  hooks: {
    beforeRequest: [
      async (options) => {
        try {
          // Potentially failing operation
          const token = await getAuthToken();
          options.headers.Authorization = `Bearer ${token}`;
        } catch (error) {
          console.warn("Failed to get auth token:", error.message);
          // Continue without auth rather than failing
        }
      }
    ],
    afterResponse: [
      async (response) => {
        try {
          // Potentially failing cache operation
          await cacheResponse(response.requestUrl.toString(), response.body);
        } catch (error) {
          console.warn("Failed to cache response:", error.message);
          // Continue with response even if caching fails
        }
        return response;
      }
    ],
    beforeError: [
      (error) => {
        try {
          // Enhance error with additional context
          const enhancedError = new Error(`Enhanced: ${error.message}`);
          enhancedError.name = error.name;
          enhancedError.stack = error.stack;
          return enhancedError as RequestError;
        } catch {
          // Return original error if enhancement fails
          return error;
        }
      }
    ]
  }
});