Skip to content

Commit de6aea6

Browse files
luddd3mcollina
andauthored
fix: response error interceptor broken (#3805)
* fix: response error interceptor broken Expose the interceptor in the same manner as the others. Remove `throwOnError` as it is considered an invalid argument. * cleanup * docs --------- Co-authored-by: Matteo Collina <hello@matteocollina.com>
1 parent 20fd58c commit de6aea6

File tree

4 files changed

+88
-242
lines changed

4 files changed

+88
-242
lines changed

docs/docs/api/Dispatcher.md

Lines changed: 12 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,203 +1054,27 @@ const response = await client.request({
10541054
})
10551055
```
10561056

1057-
##### `Response Error Interceptor`
1057+
##### `responseError`
10581058

1059-
**Introduction**
1059+
The `responseError` interceptor throws an error for responses with status code errors (>= 400).
10601060

1061-
The Response Error Interceptor is designed to handle HTTP response errors efficiently. It intercepts responses and throws detailed errors for responses with status codes indicating failure (4xx, 5xx). This interceptor enhances error handling by providing structured error information, including response headers, data, and status codes.
1062-
1063-
**ResponseError Class**
1064-
1065-
The `ResponseError` class extends the `UndiciError` class and encapsulates detailed error information. It captures the response status code, headers, and data, providing a structured way to handle errors.
1066-
1067-
**Definition**
1068-
1069-
```js
1070-
class ResponseError extends UndiciError {
1071-
constructor (message, code, { headers, data }) {
1072-
super(message);
1073-
this.name = 'ResponseError';
1074-
this.message = message || 'Response error';
1075-
this.code = 'UND_ERR_RESPONSE';
1076-
this.statusCode = code;
1077-
this.data = data;
1078-
this.headers = headers;
1079-
}
1080-
}
1081-
```
1082-
1083-
**Interceptor Handler**
1084-
1085-
The interceptor's handler class extends `DecoratorHandler` and overrides methods to capture response details and handle errors based on the response status code.
1086-
1087-
**Methods**
1088-
1089-
- **onConnect**: Initializes response properties.
1090-
- **onHeaders**: Captures headers and status code. Decodes body if content type is `application/json` or `text/plain`.
1091-
- **onData**: Appends chunks to the body if status code indicates an error.
1092-
- **onComplete**: Finalizes error handling, constructs a `ResponseError`, and invokes the `onError` method.
1093-
- **onError**: Propagates errors to the handler.
1094-
1095-
**Definition**
1096-
1097-
```js
1098-
class Handler extends DecoratorHandler {
1099-
// Private properties
1100-
#handler;
1101-
#statusCode;
1102-
#contentType;
1103-
#decoder;
1104-
#headers;
1105-
#body;
1106-
1107-
constructor (opts, { handler }) {
1108-
super(handler);
1109-
this.#handler = handler;
1110-
}
1111-
1112-
onConnect (abort) {
1113-
this.#statusCode = 0;
1114-
this.#contentType = null;
1115-
this.#decoder = null;
1116-
this.#headers = null;
1117-
this.#body = '';
1118-
return this.#handler.onConnect(abort);
1119-
}
1120-
1121-
onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
1122-
this.#statusCode = statusCode;
1123-
this.#headers = headers;
1124-
this.#contentType = headers['content-type'];
1125-
1126-
if (this.#statusCode < 400) {
1127-
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers);
1128-
}
1129-
1130-
if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
1131-
this.#decoder = new TextDecoder('utf-8');
1132-
}
1133-
}
1134-
1135-
onData (chunk) {
1136-
if (this.#statusCode < 400) {
1137-
return this.#handler.onData(chunk);
1138-
}
1139-
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '';
1140-
}
1141-
1142-
onComplete (rawTrailers) {
1143-
if (this.#statusCode >= 400) {
1144-
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '';
1145-
if (this.#contentType === 'application/json') {
1146-
try {
1147-
this.#body = JSON.parse(this.#body);
1148-
} catch {
1149-
// Do nothing...
1150-
}
1151-
}
1152-
1153-
let err;
1154-
const stackTraceLimit = Error.stackTraceLimit;
1155-
Error.stackTraceLimit = 0;
1156-
try {
1157-
err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body);
1158-
} finally {
1159-
Error.stackTraceLimit = stackTraceLimit;
1160-
}
1161-
1162-
this.#handler.onError(err);
1163-
} else {
1164-
this.#handler.onComplete(rawTrailers);
1165-
}
1166-
}
1167-
1168-
onError (err) {
1169-
this.#handler.onError(err);
1170-
}
1171-
}
1172-
1173-
module.exports = (dispatch) => (opts, handler) => opts.throwOnError
1174-
? dispatch(opts, new Handler(opts, { handler }))
1175-
: dispatch(opts, handler);
1176-
```
1177-
1178-
**Tests**
1179-
1180-
Unit tests ensure the interceptor functions correctly, handling both error and non-error responses appropriately.
1181-
1182-
**Example Tests**
1183-
1184-
- **No Error if `throwOnError` is False**:
1061+
**Example**
11851062

11861063
```js
1187-
test('should not error if request is not meant to throw error', async (t) => {
1188-
const opts = { throwOnError: false };
1189-
const handler = { onError: () => {}, onData: () => {}, onComplete: () => {} };
1190-
const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete());
1191-
assert.doesNotThrow(() => interceptor(opts, handler));
1192-
});
1193-
```
1194-
1195-
- **Error if Status Code is in Specified Error Codes**:
1196-
1197-
```js
1198-
test('should error if request status code is in the specified error codes', async (t) => {
1199-
const opts = { throwOnError: true, statusCodes: [500] };
1200-
const response = { statusCode: 500 };
1201-
let capturedError;
1202-
const handler = {
1203-
onError: (err) => { capturedError = err; },
1204-
onData: () => {},
1205-
onComplete: () => {}
1206-
};
1207-
1208-
const interceptor = createResponseErrorInterceptor((opts, handler) => {
1209-
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1210-
handler.onError(new Error('Response Error'));
1211-
} else {
1212-
handler.onComplete();
1213-
}
1214-
});
1215-
1216-
interceptor({ ...opts, response }, handler);
1217-
1218-
await new Promise(resolve => setImmediate(resolve));
1219-
1220-
assert(capturedError, 'Expected error to be captured but it was not.');
1221-
assert.strictEqual(capturedError.message, 'Response Error');
1222-
assert.strictEqual(response.statusCode, 500);
1223-
});
1224-
```
1225-
1226-
- **No Error if Status Code is Not in Specified Error Codes**:
1064+
const { Client, interceptors } = require("undici");
1065+
const { responseError } = interceptors;
12271066

1228-
```js
1229-
test('should not error if request status code is not in the specified error codes', async (t) => {
1230-
const opts = { throwOnError: true, statusCodes: [500] };
1231-
const response = { statusCode: 404 };
1232-
const handler = {
1233-
onError: () => {},
1234-
onData: () => {},
1235-
onComplete: () => {}
1236-
};
1237-
1238-
const interceptor = createResponseErrorInterceptor((opts, handler) => {
1239-
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1240-
handler.onError(new Error('Response Error'));
1241-
} else {
1242-
handler.onComplete();
1243-
}
1244-
});
1067+
const client = new Client("http://example.com").compose(
1068+
responseError()
1069+
);
12451070

1246-
assert.doesNotThrow(() => interceptor({ ...opts, response }, handler));
1071+
// Will throw a ResponseError for status codes >= 400
1072+
await client.request({
1073+
method: "GET",
1074+
path: "/"
12471075
});
12481076
```
12491077

1250-
**Conclusion**
1251-
1252-
The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
1253-
12541078
##### `Cache Interceptor`
12551079

12561080
The `cache` interceptor implements client-side response caching as described in

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module.exports.DecoratorHandler = DecoratorHandler
3838
module.exports.RedirectHandler = RedirectHandler
3939
module.exports.interceptors = {
4040
redirect: require('./lib/interceptor/redirect'),
41+
responseError: require('./lib/interceptor/response-error'),
4142
retry: require('./lib/interceptor/retry'),
4243
dump: require('./lib/interceptor/dump'),
4344
dns: require('./lib/interceptor/dns'),

lib/interceptor/response-error.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { parseHeaders } = require('../core/util')
44
const DecoratorHandler = require('../handler/decorator-handler')
55
const { ResponseError } = require('../core/errors')
66

7-
class Handler extends DecoratorHandler {
7+
class ResponseErrorHandler extends DecoratorHandler {
88
#handler
99
#statusCode
1010
#contentType
@@ -84,6 +84,10 @@ class Handler extends DecoratorHandler {
8484
}
8585
}
8686

87-
module.exports = (dispatch) => (opts, handler) => opts.throwOnError
88-
? dispatch(opts, new Handler(opts, { handler }))
89-
: dispatch(opts, handler)
87+
module.exports = () => {
88+
return (dispatch) => {
89+
return function Intercept (opts, handler) {
90+
return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
91+
}
92+
}
93+
}
Lines changed: 67 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,84 @@
11
'use strict'
22

3-
const assert = require('assert')
4-
const { test } = require('node:test')
5-
const createResponseErrorInterceptor = require('../../lib/interceptor/response-error')
6-
7-
test('should not error if request is not meant to throw error', async (t) => {
8-
const opts = { throwOnError: false }
9-
const handler = {
10-
onError: () => {},
11-
onData: () => {},
12-
onComplete: () => {}
13-
}
3+
const assert = require('node:assert')
4+
const { once } = require('node:events')
5+
const { createServer } = require('node:http')
6+
const { test, after } = require('node:test')
7+
const { interceptors, Client } = require('../..')
8+
const { responseError } = interceptors
149

15-
const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete())
10+
test('should throw error for error response', async () => {
11+
const server = createServer()
1612

17-
assert.doesNotThrow(() => interceptor(opts, handler))
18-
})
13+
server.on('request', (req, res) => {
14+
res.writeHead(400, { 'content-type': 'text/plain' })
15+
res.end('Bad Request')
16+
})
17+
18+
server.listen(0)
19+
20+
await once(server, 'listening')
21+
22+
const client = new Client(
23+
`http://localhost:${server.address().port}`
24+
).compose(responseError())
25+
26+
after(async () => {
27+
await client.close()
28+
server.close()
29+
30+
await once(server, 'close')
31+
})
1932

20-
test('should error if request status code is in the specified error codes', async (t) => {
21-
const opts = { throwOnError: true, statusCodes: [500] }
22-
const response = { statusCode: 500 }
23-
let capturedError
24-
const handler = {
25-
onError: (err) => {
26-
capturedError = err
27-
},
28-
onData: () => {},
29-
onComplete: () => {}
33+
let error
34+
try {
35+
await client.request({
36+
method: 'GET',
37+
path: '/',
38+
headers: {
39+
'content-type': 'text/plain'
40+
}
41+
})
42+
} catch (err) {
43+
error = err
3044
}
3145

32-
const interceptor = createResponseErrorInterceptor((opts, handler) => {
33-
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
34-
handler.onError(new Error('Response Error'))
35-
} else {
36-
handler.onComplete()
37-
}
46+
assert.equal(error.statusCode, 400)
47+
assert.equal(error.message, 'Response Error')
48+
assert.equal(error.data, 'Bad Request')
49+
})
50+
51+
test('should not throw error for ok response', async () => {
52+
const server = createServer()
53+
54+
server.on('request', (req, res) => {
55+
res.writeHead(200, { 'content-type': 'text/plain' })
56+
res.end('hello')
3857
})
3958

40-
interceptor({ ...opts, response }, handler)
59+
server.listen(0)
4160

42-
await new Promise(resolve => setImmediate(resolve))
61+
await once(server, 'listening')
4362

44-
assert(capturedError, 'Expected error to be captured but it was not.')
45-
assert.strictEqual(capturedError.message, 'Response Error')
46-
assert.strictEqual(response.statusCode, 500)
47-
})
63+
const client = new Client(
64+
`http://localhost:${server.address().port}`
65+
).compose(responseError())
4866

49-
test('should not error if request status code is not in the specified error codes', async (t) => {
50-
const opts = { throwOnError: true, statusCodes: [500] }
51-
const response = { statusCode: 404 }
52-
const handler = {
53-
onError: () => {},
54-
onData: () => {},
55-
onComplete: () => {}
56-
}
67+
after(async () => {
68+
await client.close()
69+
server.close()
70+
71+
await once(server, 'close')
72+
})
5773

58-
const interceptor = createResponseErrorInterceptor((opts, handler) => {
59-
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
60-
handler.onError(new Error('Response Error'))
61-
} else {
62-
handler.onComplete()
74+
const response = await client.request({
75+
method: 'GET',
76+
path: '/',
77+
headers: {
78+
'content-type': 'text/plain'
6379
}
6480
})
6581

66-
assert.doesNotThrow(() => interceptor({ ...opts, response }, handler))
82+
assert.equal(response.statusCode, 200)
83+
assert.equal(await response.body.text(), 'hello')
6784
})

0 commit comments

Comments
 (0)