Request lifecycle hooks for customizing behavior at different stages of the request process, enabling middleware-like functionality and advanced request/response processing.
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[];
}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");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;
}
}
]
}
});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");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);
}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");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 hookHooks 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");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());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;
}
}
]
}
});