Use ky hooks when several requests share authentication, request changes, caching, or error handling. The hooks run at defined points in the request lifecycle, so put each change at the point where its inputs exist.
Attach authentication and request metadata
Create an instance with beforeRequest when every request needs the current token. The hook receives the normalized request and can change its headers immediately before Ky sends it. Pass per-request values through context rather than adding them to the URL or body.
tsimport ky from 'ky';
const getToken = (): string => 'secret123';
const api = ky.create({
hooks: {
beforeRequest: [({request, options}) => {
const token = options.context.token;
if (typeof token === 'string') {
request.headers.set('Authorization', `Bearer ${token}`);
}
request.headers.set('X-Client', 'example-app');
}],
},
});
const run = async (): Promise<void> => {
const profile = await api.get('https://api.example.com/profile', {
context: {token: getToken()},
}).json<{name: string}>();
console.log(profile.name);
};
void run();
The outgoing request has Authorization: Bearer secret123 and X-Client: example-app. beforeRequest runs once for the initial request; it does not run again for retries. To alter options before Ky constructs the request, use init instead. init is synchronous.
Refresh authentication before a retry
Configure the status that can be retried, then refresh the token in beforeRetry. This hook runs only after Ky has selected a retry, so a 401 response does not refresh unless 401 is included in retry.statusCodes.
tsimport ky from 'ky';
const refreshToken = async (): Promise<string> => 'fresh-token';
let accountAttempts = 0;
const api = ky.create({
retry: {statusCodes: [401]},
hooks: {
beforeRequest: [() => {
if (accountAttempts === 0) {
accountAttempts++;
return new Response('Unauthorized', {status: 401});
}
}],
beforeRetry: [async ({request}) => {
const token = await refreshToken();
request.headers.set('Authorization', `Bearer ${token}`);
}],
},
});
const run = async (): Promise<void> => {
try {
const account = await api.get('https://api.example.com/account').json<{id: string}>();
console.log(account.id);
} catch (error) {
console.log(error instanceof Error ? error.message : 'Request failed after retry');
}
};
void run();
The retry sends the refreshed Authorization header and then returns the account response. retryCount is at least 1 in this hook. A beforeRetry hook can return a replacement Request, return a Response to skip the retry, throw to propagate an error, or return ky.stop to stop without propagating an error. A replacement request is used as-is, so remove credentials before sending it to another origin.
Return a cached response
Return a Response from beforeRequest to satisfy a request without making an HTTP request. Cache a clone in afterResponse, because the response body can only be consumed once.
tsimport ky from 'ky';
const responses = new Map<string, Response>();
responses.set('https://api.example.com/catalog', new Response(JSON.stringify({items: ['cached-item']}), {
headers: {'Content-Type': 'application/json'},
}));
const api = ky.create({
hooks: {
beforeRequest: [({request}) => {
const cached = responses.get(request.url);
return cached?.clone();
}],
afterResponse: [({request, response}) => {
responses.set(request.url, response.clone());
return response;
}],
},
});
const run = async (): Promise<void> => {
const catalog = await api.get('https://api.example.com/catalog').json<{items: string[]}>();
console.log(catalog);
};
void run();
The call receives a cached clone from the map and does not make an HTTP request. Returning a cached response also skips the remaining beforeRequest hooks.
Transform errors before they are thrown
Use beforeError to change the error that Ky is about to throw. This adds a final transformation step: return the Error you want the caller to receive, as this example changes its name and message. For HTTP-error narrowing and the consumed response-body details, see Send JSON and read responses.
tsimport ky, {isHTTPError} from 'ky';
const api = ky.create({
hooks: {
beforeRequest: [() => new Response(JSON.stringify({message: 'Not found'}), {
status: 404,
headers: {'Content-Type': 'application/json'},
})],
beforeError: [({error}) => {
if (isHTTPError(error)) {
error.name = 'ApiError';
error.message = `Request failed with status ${error.response.status}`;
}
return error;
}],
},
});
const run = async (): Promise<void> => {
try {
await api.get('https://api.example.com/missing').json<{id: string}>();
} catch (error) {
console.log(error instanceof Error ? error.message : 'Unknown failure');
}
};
void run();
The thrown error has the changed name and message for an HTTP failure. beforeError receives Ky lifecycle errors such as HTTP, network, and timeout errors and must return an Error instance. Errors raised while a returned response is being consumed are handled through a body shortcut such as .json().
Choose the hook and preserve defaults
Use these hook responsibilities:
| Hook | Use it for | Result |
|---|---|---|
init | Change mutable options before request construction | Options change; the hook is synchronous |
beforeRequest | Add authentication or replace the initial request | A request, a response that avoids the network, or no replacement |
beforeRetry | Refresh credentials or modify a selected retry | A replacement request, a response that skips retry, ky.stop, or no replacement |
afterResponse | Inspect or replace a response, or force a retry with ky.retry() | Ky uses a returned Response; ky.retry() starts a retry |
beforeError | Rename or replace an error before it is thrown | Ky throws the returned Error |
When you extend an instance, Ky deep-merges options. That appends hook arrays and merges headers. Use replaceOption when the child instance must replace a merged value instead of inheriting it.
Was this page helpful?