0
# Hooks System
1
2
Extensible lifecycle hooks for request modification, response processing, error handling, and retry customization. Hooks enable powerful middleware-like functionality for request/response transformation.
3
4
## Capabilities
5
6
### Hooks Interface
7
8
Configure lifecycle hooks that run at different stages of the request process.
9
10
```typescript { .api }
11
interface Hooks {
12
/** Modify request before sending */
13
beforeRequest?: BeforeRequestHook[];
14
/** Modify request before retry attempts */
15
beforeRetry?: BeforeRetryHook[];
16
/** Process response after receiving */
17
afterResponse?: AfterResponseHook[];
18
/** Modify HTTPError before throwing */
19
beforeError?: BeforeErrorHook[];
20
}
21
22
type BeforeRequestHook = (
23
request: KyRequest,
24
options: NormalizedOptions
25
) => Request | Response | void | Promise<Request | Response | void>;
26
27
type BeforeRetryState = {
28
request: KyRequest;
29
options: NormalizedOptions;
30
error: Error;
31
retryCount: number;
32
};
33
34
type BeforeRetryHook = (options: BeforeRetryState) => typeof stop | void | Promise<typeof stop | void>;
35
36
type AfterResponseHook = (
37
request: KyRequest,
38
options: NormalizedOptions,
39
response: KyResponse
40
) => Response | void | Promise<Response | void>;
41
42
type BeforeErrorHook = (error: HTTPError) => HTTPError | Promise<HTTPError>;
43
```
44
45
### Before Request Hooks
46
47
Modify requests before they are sent, or provide cached responses.
48
49
```typescript { .api }
50
type BeforeRequestHook = (
51
request: KyRequest,
52
options: NormalizedOptions
53
) => Request | Response | void | Promise<Request | Response | void>;
54
```
55
56
**Usage Examples:**
57
58
```typescript
59
import ky from "ky";
60
61
// Add authentication headers
62
const authClient = ky.create({
63
hooks: {
64
beforeRequest: [
65
(request) => {
66
const token = localStorage.getItem("authToken");
67
if (token) {
68
request.headers.set("Authorization", `Bearer ${token}`);
69
}
70
}
71
]
72
}
73
});
74
75
// Add request logging
76
const loggedClient = ky.create({
77
hooks: {
78
beforeRequest: [
79
(request, options) => {
80
console.log(`→ ${request.method} ${request.url}`);
81
console.log("Headers:", Object.fromEntries(request.headers));
82
if (options.json) {
83
console.log("JSON Body:", options.json);
84
}
85
}
86
]
87
}
88
});
89
90
// Request transformation
91
const transformClient = ky.create({
92
hooks: {
93
beforeRequest: [
94
(request) => {
95
// Add API version header
96
request.headers.set("API-Version", "2.0");
97
98
// Add request ID for tracing
99
request.headers.set("X-Request-ID", crypto.randomUUID());
100
101
// Add client information
102
request.headers.set("User-Agent", "MyApp/1.0");
103
}
104
]
105
}
106
});
107
108
// Return cached response
109
const cacheClient = ky.create({
110
hooks: {
111
beforeRequest: [
112
async (request) => {
113
const cacheKey = `${request.method}:${request.url}`;
114
const cached = localStorage.getItem(cacheKey);
115
116
if (cached) {
117
const { data, timestamp } = JSON.parse(cached);
118
const age = Date.now() - timestamp;
119
120
// Use cache if less than 5 minutes old
121
if (age < 5 * 60 * 1000) {
122
return new Response(JSON.stringify(data), {
123
status: 200,
124
headers: { "Content-Type": "application/json" }
125
});
126
}
127
}
128
}
129
]
130
}
131
});
132
133
// Modify request based on conditions
134
const conditionalClient = ky.create({
135
hooks: {
136
beforeRequest: [
137
(request, options) => {
138
// Add compression header for large requests
139
if (options.json && JSON.stringify(options.json).length > 1000) {
140
request.headers.set("Accept-Encoding", "gzip, deflate, br");
141
}
142
143
// Switch to alternative endpoint for specific paths
144
if (request.url.includes("/v1/")) {
145
const newUrl = request.url.replace("/v1/", "/v2/");
146
return new Request(newUrl, request);
147
}
148
}
149
]
150
}
151
});
152
```
153
154
### Before Retry Hooks
155
156
Customize retry behavior and modify requests before retry attempts.
157
158
```typescript { .api }
159
interface BeforeRetryState {
160
request: KyRequest;
161
options: NormalizedOptions;
162
error: Error;
163
retryCount: number;
164
}
165
166
type BeforeRetryHook = (state: BeforeRetryState) => typeof stop | void | Promise<typeof stop | void>;
167
```
168
169
**Usage Examples:**
170
171
```typescript
172
import ky from "ky";
173
174
// Token refresh on authentication errors
175
const authRetryClient = ky.create({
176
hooks: {
177
beforeRetry: [
178
async ({ request, options, error, retryCount }) => {
179
if (error instanceof HTTPError && error.response.status === 401) {
180
console.log(`Authentication failed, refreshing token (attempt ${retryCount})`);
181
182
try {
183
const newToken = await refreshAuthToken();
184
request.headers.set("Authorization", `Bearer ${newToken}`);
185
localStorage.setItem("authToken", newToken);
186
} catch (refreshError) {
187
console.error("Token refresh failed:", refreshError);
188
return ky.stop; // Stop retrying
189
}
190
}
191
}
192
]
193
}
194
});
195
196
// Intelligent retry decisions
197
const smartRetryClient = ky.create({
198
hooks: {
199
beforeRetry: [
200
async ({ request, options, error, retryCount }) => {
201
console.log(`Retry attempt ${retryCount} for ${request.method} ${request.url}`);
202
203
// Stop retrying after business hours for non-critical requests
204
const now = new Date();
205
const hour = now.getHours();
206
const isBusinessHours = hour >= 9 && hour < 17;
207
208
if (!isBusinessHours && retryCount > 2) {
209
console.log("Outside business hours, stopping retries");
210
return ky.stop;
211
}
212
213
// Check system health before retrying
214
if (error instanceof HTTPError && error.response.status >= 500) {
215
try {
216
const healthCheck = await ky.get("https://api.example.com/health", {
217
timeout: 2000
218
});
219
220
if (!healthCheck.ok) {
221
console.log("System unhealthy, stopping retries");
222
return ky.stop;
223
}
224
} catch {
225
console.log("Health check failed, stopping retries");
226
return ky.stop;
227
}
228
}
229
}
230
]
231
}
232
});
233
234
// Dynamic request modification
235
const dynamicRetryClient = ky.create({
236
hooks: {
237
beforeRetry: [
238
({ request, error, retryCount }) => {
239
// Reduce timeout on retry
240
if (retryCount > 1) {
241
const newTimeout = Math.max(5000 - (retryCount * 1000), 1000);
242
// Note: Can't modify timeout here, but can log strategy
243
console.log(`Retry ${retryCount}: would use timeout ${newTimeout}ms`);
244
}
245
246
// Switch to backup endpoint
247
if (retryCount > 2 && request.url.includes("api.example.com")) {
248
const backupUrl = request.url.replace("api.example.com", "backup-api.example.com");
249
// Create new request with backup URL
250
Object.defineProperty(request, "url", { value: backupUrl });
251
}
252
253
// Add retry metadata
254
request.headers.set("X-Retry-Count", retryCount.toString());
255
request.headers.set("X-Original-Error", error.message);
256
}
257
]
258
}
259
});
260
261
// Circuit breaker pattern
262
let failureCount = 0;
263
let lastFailureTime = 0;
264
const CIRCUIT_BREAKER_THRESHOLD = 5;
265
const CIRCUIT_BREAKER_TIMEOUT = 60000; // 1 minute
266
267
const circuitBreakerClient = ky.create({
268
hooks: {
269
beforeRetry: [
270
({ error, retryCount }) => {
271
const now = Date.now();
272
273
// Reset failure count after timeout
274
if (now - lastFailureTime > CIRCUIT_BREAKER_TIMEOUT) {
275
failureCount = 0;
276
}
277
278
// Increment failure count
279
if (error instanceof HTTPError && error.response.status >= 500) {
280
failureCount++;
281
lastFailureTime = now;
282
}
283
284
// Stop retrying if circuit breaker is open
285
if (failureCount >= CIRCUIT_BREAKER_THRESHOLD) {
286
console.log("Circuit breaker open, stopping retries");
287
return ky.stop;
288
}
289
}
290
]
291
}
292
});
293
```
294
295
### After Response Hooks
296
297
Process and potentially modify responses after they are received.
298
299
```typescript { .api }
300
type AfterResponseHook = (
301
request: KyRequest,
302
options: NormalizedOptions,
303
response: KyResponse
304
) => Response | void | Promise<Response | void>;
305
```
306
307
**Usage Examples:**
308
309
```typescript
310
import ky from "ky";
311
312
// Response logging
313
const loggedClient = ky.create({
314
hooks: {
315
afterResponse: [
316
(request, options, response) => {
317
console.log(`← ${response.status} ${request.method} ${request.url}`);
318
console.log("Response Headers:", Object.fromEntries(response.headers));
319
320
// Log timing information
321
const duration = Date.now() - (request as any).startTime;
322
console.log(`Duration: ${duration}ms`);
323
}
324
]
325
}
326
});
327
328
// Response caching
329
const cachingClient = ky.create({
330
hooks: {
331
afterResponse: [
332
async (request, options, response) => {
333
// Only cache successful GET requests
334
if (request.method === "GET" && response.ok) {
335
const cacheKey = `${request.method}:${request.url}`;
336
const data = await response.clone().json();
337
338
localStorage.setItem(cacheKey, JSON.stringify({
339
data,
340
timestamp: Date.now(),
341
headers: Object.fromEntries(response.headers)
342
}));
343
}
344
}
345
]
346
}
347
});
348
349
// Response transformation
350
const transformResponseClient = ky.create({
351
hooks: {
352
afterResponse: [
353
async (request, options, response) => {
354
// Unwrap API responses
355
if (response.headers.get("content-type")?.includes("application/json")) {
356
const data = await response.clone().json();
357
358
// If response has a "data" wrapper, unwrap it
359
if (data && typeof data === "object" && "data" in data) {
360
return new Response(JSON.stringify(data.data), {
361
status: response.status,
362
statusText: response.statusText,
363
headers: response.headers
364
});
365
}
366
}
367
}
368
]
369
}
370
});
371
372
// Automatic retry on specific conditions
373
const conditionalRetryClient = ky.create({
374
hooks: {
375
afterResponse: [
376
async (request, options, response) => {
377
// Retry on 403 with token refresh
378
if (response.status === 403) {
379
const newToken = await refreshAuthToken();
380
381
// Create new request with fresh token
382
const newRequest = new Request(request, {
383
headers: {
384
...Object.fromEntries(request.headers),
385
"Authorization": `Bearer ${newToken}`
386
}
387
});
388
389
// Retry the request
390
return ky(newRequest, options);
391
}
392
}
393
]
394
}
395
});
396
397
// Response monitoring and metrics
398
const metricsClient = ky.create({
399
hooks: {
400
afterResponse: [
401
(request, options, response) => {
402
// Send metrics to monitoring service
403
const metrics = {
404
method: request.method,
405
url: request.url,
406
status: response.status,
407
duration: Date.now() - (request as any).startTime,
408
size: response.headers.get("content-length") || 0
409
};
410
411
// Send to analytics (non-blocking)
412
sendMetrics(metrics).catch(console.warn);
413
414
// Update performance counters
415
updatePerformanceCounters(metrics);
416
}
417
]
418
}
419
});
420
```
421
422
### Before Error Hooks
423
424
Modify HTTPError objects before they are thrown.
425
426
```typescript { .api }
427
type BeforeErrorHook = (error: HTTPError) => HTTPError | Promise<HTTPError>;
428
```
429
430
**Usage Examples:**
431
432
```typescript
433
import ky from "ky";
434
435
// Enhanced error information
436
const enhancedErrorClient = ky.create({
437
hooks: {
438
beforeError: [
439
async (error) => {
440
// Add response body to error for debugging
441
if (error.response.body) {
442
try {
443
const responseText = await error.response.clone().text();
444
error.message += `\nResponse body: ${responseText}`;
445
} catch {
446
// Ignore if body can't be read
447
}
448
}
449
450
// Add request information
451
error.message += `\nRequest: ${error.request.method} ${error.request.url}`;
452
453
// Add timestamp
454
error.message += `\nTimestamp: ${new Date().toISOString()}`;
455
456
return error;
457
}
458
]
459
}
460
});
461
462
// Custom error types
463
class APIError extends Error {
464
constructor(
465
message: string,
466
public code: string,
467
public status: number
468
) {
469
super(message);
470
this.name = "APIError";
471
}
472
}
473
474
const customErrorClient = ky.create({
475
hooks: {
476
beforeError: [
477
async (error) => {
478
try {
479
const errorData = await error.response.clone().json();
480
481
// Transform to custom error type
482
if (errorData.code && errorData.message) {
483
const customError = new APIError(
484
errorData.message,
485
errorData.code,
486
error.response.status
487
);
488
489
// Copy properties from original error
490
(customError as any).response = error.response;
491
(customError as any).request = error.request;
492
(customError as any).options = error.options;
493
494
return customError as any;
495
}
496
} catch {
497
// Fall back to original error if parsing fails
498
}
499
500
return error;
501
}
502
]
503
}
504
});
505
506
// Error reporting and logging
507
const reportingClient = ky.create({
508
hooks: {
509
beforeError: [
510
(error) => {
511
// Log error details
512
console.error("HTTP Error:", {
513
method: error.request.method,
514
url: error.request.url,
515
status: error.response.status,
516
statusText: error.response.statusText,
517
message: error.message
518
});
519
520
// Report to error tracking service
521
if (error.response.status >= 500) {
522
reportError({
523
type: "http_error",
524
status: error.response.status,
525
url: error.request.url,
526
method: error.request.method,
527
message: error.message,
528
timestamp: new Date().toISOString()
529
});
530
}
531
532
return error;
533
}
534
]
535
}
536
});
537
538
// User-friendly error messages
539
const userFriendlyClient = ky.create({
540
hooks: {
541
beforeError: [
542
(error) => {
543
// Map status codes to user-friendly messages
544
const userMessages: Record<number, string> = {
545
400: "The request was invalid. Please check your input.",
546
401: "Authentication required. Please log in.",
547
403: "You don't have permission to access this resource.",
548
404: "The requested resource was not found.",
549
429: "Too many requests. Please try again later.",
550
500: "Server error. Please try again later.",
551
502: "Service temporarily unavailable.",
552
503: "Service under maintenance. Please try again later."
553
};
554
555
const userMessage = userMessages[error.response.status];
556
if (userMessage) {
557
error.message = userMessage;
558
}
559
560
return error;
561
}
562
]
563
}
564
});
565
```
566
567
### Multiple Hooks
568
569
Chain multiple hooks for complex request/response processing.
570
571
**Usage Examples:**
572
573
```typescript
574
import ky from "ky";
575
576
// Multiple hooks in sequence
577
const multiHookClient = ky.create({
578
hooks: {
579
beforeRequest: [
580
// 1. Add authentication
581
(request) => {
582
const token = getAuthToken();
583
if (token) {
584
request.headers.set("Authorization", `Bearer ${token}`);
585
}
586
},
587
// 2. Add tracing
588
(request) => {
589
request.headers.set("X-Trace-ID", generateTraceId());
590
},
591
// 3. Add timing
592
(request) => {
593
(request as any).startTime = Date.now();
594
}
595
],
596
afterResponse: [
597
// 1. Log response
598
(request, options, response) => {
599
const duration = Date.now() - (request as any).startTime;
600
console.log(`${request.method} ${request.url} - ${response.status} (${duration}ms)`);
601
},
602
// 2. Cache if appropriate
603
async (request, options, response) => {
604
if (request.method === "GET" && response.ok) {
605
await cacheResponse(request.url, response.clone());
606
}
607
},
608
// 3. Update metrics
609
(request, options, response) => {
610
updateRequestMetrics({
611
method: request.method,
612
status: response.status,
613
duration: Date.now() - (request as any).startTime
614
});
615
}
616
]
617
}
618
});
619
```
620
621
## Types
622
623
```typescript { .api }
624
interface Hooks {
625
beforeRequest?: BeforeRequestHook[];
626
beforeRetry?: BeforeRetryHook[];
627
afterResponse?: AfterResponseHook[];
628
beforeError?: BeforeErrorHook[];
629
}
630
631
type BeforeRequestHook = (
632
request: KyRequest,
633
options: NormalizedOptions
634
) => Request | Response | void | Promise<Request | Response | void>;
635
636
interface BeforeRetryState {
637
request: KyRequest;
638
options: NormalizedOptions;
639
error: Error;
640
retryCount: number;
641
}
642
643
type BeforeRetryHook = (state: BeforeRetryState) => typeof stop | void | Promise<typeof stop | void>;
644
645
type AfterResponseHook = (
646
request: KyRequest,
647
options: NormalizedOptions,
648
response: KyResponse
649
) => Response | void | Promise<Response | void>;
650
651
type BeforeErrorHook = (error: HTTPError) => HTTPError | Promise<HTTPError>;
652
653
// Special symbol for stopping retries
654
declare const stop: unique symbol;
655
```