Human-friendly and powerful HTTP request library for Node.js
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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;
}
}
]
}
});