Skip to content

Commit 616d276

Browse files
Malolan Bsindresorhus
Malolan B
andauthoredFeb 15, 2022
Add beforeError hook (#417)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 3e72832 commit 616d276

File tree

6 files changed

+134
-2
lines changed

6 files changed

+134
-2
lines changed
 

‎readme.md

+27
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,33 @@ const response = await ky('https://example.com', {
258258
});
259259
```
260260

261+
###### hooks.beforeError
262+
263+
Type: `Function[]`\
264+
Default: `[]`
265+
266+
This hook enables you to modify the `HTTPError` right before it is thrown. The hook function receives a `HTTPError` as an argument and should return an instance of `HTTPError`.
267+
268+
```js
269+
import ky from 'ky';
270+
271+
await ky('https://example.com', {
272+
hooks: {
273+
beforeError: [
274+
error => {
275+
const {response} = error;
276+
if (response && response.body) {
277+
error.name = 'GitHubError';
278+
error.message = `${response.body.message} (${response.statusCode})`;
279+
}
280+
281+
return error;
282+
}
283+
]
284+
}
285+
});
286+
```
287+
261288
###### hooks.afterResponse
262289

263290
Type: `Function[]`\

‎source/core/Ky.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ export class Ky {
3939
ky._decorateResponse(response);
4040

4141
if (!response.ok && ky._options.throwHttpErrors) {
42-
throw new HTTPError(response, ky.request, (ky._options as unknown) as NormalizedOptions);
42+
let error = new HTTPError(response, ky.request, (ky._options as unknown) as NormalizedOptions);
43+
44+
for (const hook of ky._options.hooks.beforeError) {
45+
// eslint-disable-next-line no-await-in-loop
46+
error = await hook(error);
47+
}
48+
49+
throw error;
4350
}
4451

4552
// If `onDownloadProgress` is passed, it uses the stream API internally
@@ -104,6 +111,7 @@ export class Ky {
104111
{
105112
beforeRequest: [],
106113
beforeRetry: [],
114+
beforeError: [],
107115
afterResponse: [],
108116
},
109117
options.hooks,

‎source/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export {
3939
Hooks,
4040
BeforeRequestHook,
4141
BeforeRetryHook,
42+
BeforeErrorHook,
4243
AfterResponseHook,
4344
} from './types/hooks.js';
4445

‎source/types/hooks.ts

+31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {stop} from '../core/constants.js';
2+
import {HTTPError} from '../index.js';
23
import type {NormalizedOptions} from './options.js';
34

45
export type BeforeRequestHook = (
@@ -20,6 +21,8 @@ export type AfterResponseHook = (
2021
response: Response
2122
) => Response | void | Promise<Response | void>;
2223

24+
export type BeforeErrorHook = (error: HTTPError) => HTTPError | Promise<HTTPError>;
25+
2326
export interface Hooks {
2427
/**
2528
This hook enables you to modify the request right before it is sent. Ky will make no further changes to the request after this. The hook function receives normalized input and options as arguments. You could, forf example, modiy `options.headers` here.
@@ -95,4 +98,32 @@ export interface Hooks {
9598
```
9699
*/
97100
afterResponse?: AfterResponseHook[];
101+
102+
/**
103+
This hook enables you to modify the `HTTPError` right before it is thrown. The hook function receives a `HTTPError` as an argument and should return an instance of `HTTPError`.
104+
105+
@default []
106+
107+
@example
108+
```
109+
import ky from 'ky';
110+
111+
await ky('https://example.com', {
112+
hooks: {
113+
beforeError: [
114+
error => {
115+
const {response} = error;
116+
if (response && response.body) {
117+
error.name = 'GitHubError';
118+
error.message = `${response.body.message} (${response.statusCode})`;
119+
}
120+
121+
return error;
122+
}
123+
]
124+
}
125+
});
126+
```
127+
*/
128+
beforeError?: BeforeErrorHook[];
98129
}

‎test/helpers/with-page.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import process from 'node:process';
2-
// eslint-disable-next-line ava/use-test
32
import type {ExecutionContext, UntitledMacro} from 'ava';
43
import {chromium, Page} from 'playwright-chromium';
54

‎test/hooks.ts

+66
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,69 @@ test('hooks beforeRequest returning Response skips HTTP Request', async t => {
602602

603603
t.is(response, expectedResponse);
604604
});
605+
606+
test('runs beforeError before throwing HTTPError', async t => {
607+
const server = await createHttpTestServer();
608+
server.post('/', (_request, response) => {
609+
response.status(500).send();
610+
});
611+
612+
await t.throwsAsync(
613+
ky.post(server.url, {
614+
hooks: {
615+
beforeError: [
616+
(error: HTTPError) => {
617+
const {response} = error;
618+
619+
if (response?.body) {
620+
error.name = 'GitHubError';
621+
error.message = `${response.statusText} --- (${response.status})`.trim();
622+
}
623+
624+
return error;
625+
},
626+
],
627+
},
628+
}),
629+
{
630+
name: 'GitHubError',
631+
message: 'Internal Server Error --- (500)',
632+
},
633+
);
634+
635+
await server.close();
636+
});
637+
638+
test('beforeError can return promise which resolves to HTTPError', async t => {
639+
const server = await createHttpTestServer();
640+
const responseBody = {reason: 'github down'};
641+
server.post('/', (_request, response) => {
642+
response.status(500).send(responseBody);
643+
});
644+
645+
await t.throwsAsync(
646+
ky.post(server.url, {
647+
hooks: {
648+
beforeError: [
649+
async (error: HTTPError) => {
650+
const {response} = error;
651+
const body = await response.json() as {reason: string};
652+
653+
if (response?.body) {
654+
error.name = 'GitHubError';
655+
error.message = `${body.reason} --- (${response.status})`.trim();
656+
}
657+
658+
return error;
659+
},
660+
],
661+
},
662+
}),
663+
{
664+
name: 'GitHubError',
665+
message: `${responseBody.reason} --- (500)`,
666+
},
667+
);
668+
669+
await server.close();
670+
});

0 commit comments

Comments
 (0)
Please sign in to comment.