Skip to content

Commit 4de7a4d

Browse files
committed
Add request property to HttpError
1 parent 4137bb5 commit 4de7a4d

File tree

9 files changed

+76
-46
lines changed

9 files changed

+76
-46
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const config = {
2424
'no-underscore-dangle': 'off',
2525
'no-plusplus': 'off',
2626
'spaced-comment': 'off',
27+
'lines-between-class-members': 'off',
2728
camelcase: 'off',
2829

2930
'import/no-extraneous-dependencies': 'off',

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Check [examples/web](examples/web)
123123

124124
### HttpError
125125

126-
@tkrotoff/fetch throws [`HttpError`](src/HttpError.ts) with a [`response`](https://fetch.spec.whatwg.org/#response) property when the HTTP status code is < `200` or >= `300`.
126+
@tkrotoff/fetch throws [`HttpError`](src/HttpError.ts) with [`response`](https://fetch.spec.whatwg.org/#response) and [`request`](https://fetch.spec.whatwg.org/#request) properties when the HTTP status code is < `200` or >= `300`.
127127

128128
### Test utilities
129129

src/Http.test.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { defaults, del, get, patch, patchJSON, post, postJSON, put, putJSON } fr
77
const path = '/';
88

99
test('defaults.init', async () => {
10-
const url = 'http://localhost';
10+
const url = 'http://localhost/';
1111

1212
const spy = jest
1313
.spyOn(globalThis, 'fetch')
@@ -17,12 +17,11 @@ test('defaults.init', async () => {
1717
// Should use defaults.init
1818
await get(url).text();
1919
expect(spy).toHaveBeenCalledTimes(1);
20-
expect(spy).toHaveBeenLastCalledWith(url, {
21-
headers: expect.any(Headers),
22-
method: 'GET'
23-
});
24-
let headers = entriesToObject(spy.mock.calls[0][1]!.headers as Headers);
25-
expect(headers).toEqual({ accept: 'text/*' });
20+
expect(spy).toHaveBeenLastCalledWith(expect.any(Request));
21+
let req = spy.mock.calls[0][0] as Request;
22+
expect(req.method).toEqual('GET');
23+
expect(req.url).toEqual(url);
24+
expect(entriesToObject(req.headers)).toEqual({ accept: 'text/*' });
2625

2726
// What happens when defaults.init is modified?
2827
const originalInit = { ...defaults.init };
@@ -35,27 +34,25 @@ test('defaults.init', async () => {
3534
spy.mockClear();
3635
await get(url).text();
3736
expect(spy).toHaveBeenCalledTimes(1);
38-
expect(spy).toHaveBeenLastCalledWith(url, {
39-
mode: 'cors',
40-
credentials: 'include',
41-
headers: expect.any(Headers),
42-
method: 'GET'
43-
});
44-
headers = entriesToObject(spy.mock.calls[0][1]!.headers as Headers);
45-
expect(headers).toEqual({ accept: 'text/*', test1: 'true' });
37+
expect(spy).toHaveBeenLastCalledWith(expect.any(Request));
38+
req = spy.mock.calls[0][0] as Request;
39+
expect(req.method).toEqual('GET');
40+
expect(req.url).toEqual(url);
41+
expect(req.mode).toEqual('cors');
42+
expect(req.credentials).toEqual('include');
43+
expect(entriesToObject(req.headers)).toEqual({ accept: 'text/*', test1: 'true' });
4644

4745
// Should not overwrite defaults.init.headers
4846
spy.mockClear();
4947
await get(url, { mode: 'no-cors', credentials: 'omit', headers: { test2: 'true' } }).text();
5048
expect(spy).toHaveBeenCalledTimes(1);
51-
expect(spy).toHaveBeenLastCalledWith(url, {
52-
mode: 'no-cors',
53-
credentials: 'omit',
54-
headers: expect.any(Headers),
55-
method: 'GET'
56-
});
57-
headers = entriesToObject(spy.mock.calls[0][1]!.headers as Headers);
58-
expect(headers).toEqual({ accept: 'text/*', test1: 'true', test2: 'true' });
49+
expect(spy).toHaveBeenLastCalledWith(expect.any(Request));
50+
req = spy.mock.calls[0][0] as Request;
51+
expect(req.method).toEqual('GET');
52+
expect(req.url).toEqual(url);
53+
expect(req.mode).toEqual('no-cors');
54+
expect(req.credentials).toEqual('omit');
55+
expect(entriesToObject(req.headers)).toEqual({ accept: 'text/*', test1: 'true', test2: 'true' });
5956

6057
defaults.init = originalInit;
6158
expect(defaults.init).toEqual({});

src/Http.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,19 @@ function request<T extends BodyInit>(
9595
// Have to wait for headers to be modified inside extendResponsePromiseWithBodyMethods
9696
await Promise.resolve();
9797

98-
const response = await fetch(input, {
98+
const req = new Request(input, {
9999
...defaults.init,
100100
...init,
101101
headers,
102102
method,
103103
body
104104
});
105105

106-
if (!response.ok) throw new HttpError(response);
106+
const res = await fetch(req);
107107

108-
return response;
108+
if (!res.ok) throw new HttpError(req, res);
109+
110+
return res;
109111
}
110112

111113
const responsePromise = _fetch() as ResponsePromiseWithBodyMethods;

src/HttpError.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { HttpError } from './HttpError';
77
const path = '/';
88

99
test('HttpError with statusText (HTTP/1.1)', async () => {
10-
expect.assertions(6);
10+
expect.assertions(8);
1111

1212
const server = createTestServer({ silenceErrors: true });
1313

@@ -20,10 +20,12 @@ test('HttpError with statusText (HTTP/1.1)', async () => {
2020
await get(url).text();
2121
} catch (e) {
2222
assert(e instanceof HttpError);
23-
const { name, message, response } = e;
23+
const { name, message, request, response } = e;
2424
/* eslint-disable jest/no-conditional-expect */
2525
expect(name).toEqual('HttpError');
2626
expect(message).toEqual('Not Found');
27+
expect(request.method).toEqual('GET');
28+
expect(request.url).toContain('https://127.0.0.1:');
2729
expect(response.status).toEqual(404);
2830
expect(response.statusText).toEqual('Not Found');
2931
expect(response.headers.get('content-type')).toEqual('application/json; charset=utf-8');
@@ -51,26 +53,33 @@ test('HttpError without statusText because of HTTP/2', async () => {
5153

5254
// With statusText
5355
let e = new HttpError(
56+
undefined!,
5457
new Response(JSON.stringify(body), { status: 404, statusText: 'Not Found' })
5558
);
5659
expect(e.name).toEqual('HttpError');
5760
expect(e.message).toEqual('Not Found');
61+
expect(e.request).toEqual(undefined);
5862
expect(e.response.status).toEqual(404);
5963
expect(e.response.statusText).toEqual('Not Found');
6064
expect(await e.response.json()).toEqual(body);
6165

6266
// Without statusText
63-
e = new HttpError(new Response(JSON.stringify(body), { status: 404 }));
67+
e = new HttpError(undefined!, new Response(JSON.stringify(body), { status: 404 }));
6468
expect(e.name).toEqual('HttpError');
6569
expect(e.message).toEqual('404');
70+
expect(e.request).toEqual(undefined);
6671
expect(e.response.status).toEqual(404);
6772
expect(e.response.statusText).toEqual('');
6873
expect(await e.response.json()).toEqual(body);
6974

7075
// With empty statusText
71-
e = new HttpError(new Response(JSON.stringify(body), { status: 404, statusText: '' }));
76+
e = new HttpError(
77+
undefined!,
78+
new Response(JSON.stringify(body), { status: 404, statusText: '' })
79+
);
7280
expect(e.name).toEqual('HttpError');
7381
expect(e.message).toEqual('404');
82+
expect(e.request).toEqual(undefined);
7483
expect(e.response.status).toEqual(404);
7584
expect(e.response.statusText).toEqual('');
7685
expect(await e.response.json()).toEqual(body);

src/HttpError.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
// - Node.js uses [http](https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/)
99
// - Deno uses [Http](https://github.com/denoland/deno/blob/v1.5.3/cli/rt/01_errors.js#L116)
1010
export class HttpError extends Error {
11+
/**
12+
* Undefined when using {@link createHttpError()} or {@link createResponsePromise()}.
13+
*/
14+
request: Request;
1115
response: Response;
1216

13-
constructor(response: Response) {
17+
constructor(request: Request, response: Response) {
1418
const { status, statusText } = response;
1519

1620
super(
@@ -19,6 +23,7 @@ export class HttpError extends Error {
1923
);
2024

2125
this.name = 'HttpError';
26+
this.request = request;
2227
this.response = response;
2328
}
2429
}

src/createHttpError.test.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ test('Response.error()', async () => {
138138

139139
test('200 OK', async () => {
140140
{
141-
const { name, message, response } = createHttpError('body', 200, 'OK');
141+
const { name, message, request, response } = createHttpError('body', 200, 'OK');
142142
expect(name).toEqual('HttpError');
143143
expect(message).toEqual('OK');
144+
expect(request).toEqual(undefined);
144145
checkBody(response.body);
145146
expect(response.bodyUsed).toEqual(false);
146147
expect(entriesToObject(response.headers)).toEqual({
@@ -158,9 +159,10 @@ test('200 OK', async () => {
158159
}
159160

160161
{
161-
const { name, message, response } = createJSONHttpError({ body: true }, 200, 'OK');
162+
const { name, message, request, response } = createJSONHttpError({ body: true }, 200, 'OK');
162163
expect(name).toEqual('HttpError');
163164
expect(message).toEqual('OK');
165+
expect(request).toEqual(undefined);
164166
checkBody(response.body);
165167
expect(response.bodyUsed).toEqual(false);
166168
expect(entriesToObject(response.headers)).toEqual({
@@ -180,9 +182,10 @@ test('200 OK', async () => {
180182

181183
test('204 No Content', async () => {
182184
{
183-
const { name, message, response } = createHttpError(undefined, 204, 'No Content');
185+
const { name, message, request, response } = createHttpError(undefined, 204, 'No Content');
184186
expect(name).toEqual('HttpError');
185187
expect(message).toEqual('No Content');
188+
expect(request).toEqual(undefined);
186189
expect(response.body).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : null);
187190
expect(response.bodyUsed).toEqual(false);
188191
expect(entriesToObject(response.headers)).toEqual({});
@@ -218,9 +221,10 @@ test('204 No Content', async () => {
218221

219222
test('404 Not Found', async () => {
220223
{
221-
const { name, message, response } = createHttpError('error', 404, 'Not Found');
224+
const { name, message, request, response } = createHttpError('error', 404, 'Not Found');
222225
expect(name).toEqual('HttpError');
223226
expect(message).toEqual('Not Found');
227+
expect(request).toEqual(undefined);
224228
checkBody(response.body);
225229
expect(response.bodyUsed).toEqual(false);
226230
expect(entriesToObject(response.headers)).toEqual({
@@ -238,9 +242,14 @@ test('404 Not Found', async () => {
238242
}
239243

240244
{
241-
const { name, message, response } = createJSONHttpError({ error: 404 }, 404, 'Not Found');
245+
const { name, message, request, response } = createJSONHttpError(
246+
{ error: 404 },
247+
404,
248+
'Not Found'
249+
);
242250
expect(name).toEqual('HttpError');
243251
expect(message).toEqual('Not Found');
252+
expect(request).toEqual(undefined);
244253
checkBody(response.body);
245254
expect(response.bodyUsed).toEqual(false);
246255
expect(entriesToObject(response.headers)).toEqual({
@@ -260,9 +269,10 @@ test('404 Not Found', async () => {
260269

261270
test('no statusText', async () => {
262271
{
263-
const { name, message, response } = createHttpError('body', 200);
272+
const { name, message, request, response } = createHttpError('body', 200);
264273
expect(name).toEqual('HttpError');
265274
expect(message).toEqual('200');
275+
expect(request).toEqual(undefined);
266276
checkBody(response.body);
267277
expect(response.bodyUsed).toEqual(false);
268278
expect(entriesToObject(response.headers)).toEqual({
@@ -280,9 +290,10 @@ test('no statusText', async () => {
280290
}
281291

282292
{
283-
const { name, message, response } = createJSONHttpError({ body: true }, 200);
293+
const { name, message, request, response } = createJSONHttpError({ body: true }, 200);
284294
expect(name).toEqual('HttpError');
285295
expect(message).toEqual('200');
296+
expect(request).toEqual(undefined);
286297
checkBody(response.body);
287298
expect(response.bodyUsed).toEqual(false);
288299
expect(entriesToObject(response.headers)).toEqual({
@@ -339,9 +350,10 @@ test('status 0', async () => {
339350
test('no status', async () => {
340351
{
341352
// @ts-ignore
342-
const { name, message, response } = createHttpError('body');
353+
const { name, message, request, response } = createHttpError('body');
343354
expect(name).toEqual('HttpError');
344355
expect(message).toEqual('200');
356+
expect(request).toEqual(undefined);
345357
checkBody(response.body);
346358
expect(response.bodyUsed).toEqual(false);
347359
expect(entriesToObject(response.headers)).toEqual({
@@ -360,9 +372,10 @@ test('no status', async () => {
360372

361373
{
362374
// @ts-ignore
363-
const { name, message, response } = createJSONHttpError({ body: true });
375+
const { name, message, request, response } = createJSONHttpError({ body: true });
364376
expect(name).toEqual('HttpError');
365377
expect(message).toEqual('200');
378+
expect(request).toEqual(undefined);
366379
checkBody(response.body);
367380
expect(response.bodyUsed).toEqual(false);
368381
expect(entriesToObject(response.headers)).toEqual({
@@ -382,9 +395,10 @@ test('no status', async () => {
382395

383396
test('no params', async () => {
384397
// @ts-ignore
385-
const { name, message, response } = createHttpError();
398+
const { name, message, request, response } = createHttpError();
386399
expect(name).toEqual('HttpError');
387400
expect(message).toEqual('200');
401+
expect(request).toEqual(undefined);
388402
expect(response.body).toEqual(process.env.FETCH === 'whatwg-fetch' ? undefined : null);
389403
expect(response.bodyUsed).toEqual(false);
390404
expect(entriesToObject(response.headers)).toEqual({});

src/createHttpError.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { jsonMimeType } from './Http';
22
import { HttpError } from './HttpError';
33

44
/**
5-
* Creates a {@link HttpError}.
5+
* Creates a {@link HttpError} with the given response and an undefined request.
66
*
77
* @see {@link createJSONHttpError()}
88
*/
99
export function createHttpError(body: BodyInit | undefined, status: number, statusText?: string) {
1010
return new HttpError(
11+
undefined!,
1112
new Response(body, {
1213
status,
1314
statusText
@@ -16,14 +17,15 @@ export function createHttpError(body: BodyInit | undefined, status: number, stat
1617
}
1718

1819
/**
19-
* Creates a {@link HttpError} with a JSON {@link Response} body.
20+
* Creates a {@link HttpError} with a JSON {@link Response} response body and an undefined request.
2021
*
2122
* @see {@link createHttpError()}
2223
*/
2324
// Record<string, unknown> is compatible with "type" not with "interface": "Index signature is missing in type 'MyInterface'"
2425
// Best alternative is object, why? https://stackoverflow.com/a/58143592
2526
export function createJSONHttpError(body: object, status: number, statusText?: string) {
2627
return new HttpError(
28+
undefined!,
2729
// FIXME Replace with [Response.json()](https://twitter.com/lcasdev/status/1564598435772342272)
2830
new Response(JSON.stringify(body), {
2931
status,

src/createResponsePromise.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function extendResponsePromiseWithBodyMethods(
1818
// We already reject just below (body method) and
1919
// we don't want the "root" responsePromise rejection to be unhandled
2020
});
21-
reject(new HttpError(response));
21+
reject(new HttpError(undefined!, response));
2222
}
2323
});
2424
});
@@ -58,7 +58,7 @@ export function createResponsePromise(body?: BodyInit, init?: ResponseInit) {
5858
} else {
5959
// Let's call this the "root" responsePromise rejection
6060
// Will be silently caught if we throw inside a body method, see extendResponsePromiseWithBodyMethods
61-
reject(new HttpError(response));
61+
reject(new HttpError(undefined!, response));
6262
}
6363
}) as ResponsePromiseWithBodyMethods;
6464

0 commit comments

Comments
 (0)