or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mderrors.mdhooks.mdhttp-methods.mdindex.mdinstances.mdresponses.mdretry.md
tile.json

hooks.mddocs/

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

```