Skip to content

Commit

Permalink
Add beforeError hook (#417)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Malolan B and sindresorhus committed Feb 15, 2022
1 parent 3e72832 commit 616d276
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 2 deletions.
27 changes: 27 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,33 @@ const response = await ky('https://example.com', {
});
```

###### hooks.beforeError

Type: `Function[]`\
Default: `[]`

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`.

```js
import ky from 'ky';

await ky('https://example.com', {
hooks: {
beforeError: [
error => {
const {response} = error;
if (response && response.body) {
error.name = 'GitHubError';
error.message = `${response.body.message} (${response.statusCode})`;
}

return error;
}
]
}
});
```

###### hooks.afterResponse

Type: `Function[]`\
Expand Down
10 changes: 9 additions & 1 deletion source/core/Ky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ export class Ky {
ky._decorateResponse(response);

if (!response.ok && ky._options.throwHttpErrors) {
throw new HTTPError(response, ky.request, (ky._options as unknown) as NormalizedOptions);
let error = new HTTPError(response, ky.request, (ky._options as unknown) as NormalizedOptions);

for (const hook of ky._options.hooks.beforeError) {
// eslint-disable-next-line no-await-in-loop
error = await hook(error);
}

throw error;
}

// If `onDownloadProgress` is passed, it uses the stream API internally
Expand Down Expand Up @@ -104,6 +111,7 @@ export class Ky {
{
beforeRequest: [],
beforeRetry: [],
beforeError: [],
afterResponse: [],
},
options.hooks,
Expand Down
1 change: 1 addition & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export {
Hooks,
BeforeRequestHook,
BeforeRetryHook,
BeforeErrorHook,
AfterResponseHook,
} from './types/hooks.js';

Expand Down
31 changes: 31 additions & 0 deletions source/types/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {stop} from '../core/constants.js';
import {HTTPError} from '../index.js';
import type {NormalizedOptions} from './options.js';

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

export type BeforeErrorHook = (error: HTTPError) => HTTPError | Promise<HTTPError>;

export interface Hooks {
/**
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.
Expand Down Expand Up @@ -95,4 +98,32 @@ export interface Hooks {
```
*/
afterResponse?: AfterResponseHook[];

/**
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`.
@default []
@example
```
import ky from 'ky';
await ky('https://example.com', {
hooks: {
beforeError: [
error => {
const {response} = error;
if (response && response.body) {
error.name = 'GitHubError';
error.message = `${response.body.message} (${response.statusCode})`;
}
return error;
}
]
}
});
```
*/
beforeError?: BeforeErrorHook[];
}
1 change: 0 additions & 1 deletion test/helpers/with-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import process from 'node:process';
// eslint-disable-next-line ava/use-test
import type {ExecutionContext, UntitledMacro} from 'ava';
import {chromium, Page} from 'playwright-chromium';

Expand Down
66 changes: 66 additions & 0 deletions test/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,3 +602,69 @@ test('hooks beforeRequest returning Response skips HTTP Request', async t => {

t.is(response, expectedResponse);
});

test('runs beforeError before throwing HTTPError', async t => {
const server = await createHttpTestServer();
server.post('/', (_request, response) => {
response.status(500).send();
});

await t.throwsAsync(
ky.post(server.url, {
hooks: {
beforeError: [
(error: HTTPError) => {
const {response} = error;

if (response?.body) {
error.name = 'GitHubError';
error.message = `${response.statusText} --- (${response.status})`.trim();
}

return error;
},
],
},
}),
{
name: 'GitHubError',
message: 'Internal Server Error --- (500)',
},
);

await server.close();
});

test('beforeError can return promise which resolves to HTTPError', async t => {
const server = await createHttpTestServer();
const responseBody = {reason: 'github down'};
server.post('/', (_request, response) => {
response.status(500).send(responseBody);
});

await t.throwsAsync(
ky.post(server.url, {
hooks: {
beforeError: [
async (error: HTTPError) => {
const {response} = error;
const body = await response.json() as {reason: string};

if (response?.body) {
error.name = 'GitHubError';
error.message = `${body.reason} --- (${response.status})`.trim();
}

return error;
},
],
},
}),
{
name: 'GitHubError',
message: `${responseBody.reason} --- (500)`,
},
);

await server.close();
});

0 comments on commit 616d276

Please sign in to comment.