undici 7.0.0-alpha.8 → 7.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Dispatcher.md +13 -189
- package/index.js +1 -0
- package/lib/core/errors.js +2 -2
- package/lib/dispatcher/dispatcher-base.js +4 -2
- package/lib/handler/cache-handler.js +4 -4
- package/lib/handler/cache-revalidation-handler.js +5 -5
- package/lib/handler/redirect-handler.js +2 -2
- package/lib/handler/retry-handler.js +65 -100
- package/lib/handler/unwrap-handler.js +1 -1
- package/lib/handler/wrap-handler.js +2 -2
- package/lib/interceptor/cache.js +1 -4
- package/lib/interceptor/response-error.js +9 -5
- package/package.json +1 -1
- package/types/dispatcher.d.ts +1 -1
|
@@ -207,7 +207,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
|
|
|
207
207
|
|
|
208
208
|
* **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
|
|
209
209
|
* **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string[]>, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
|
|
210
|
-
* **onResponseStart** `(controller: DispatchController, statusCode: number,
|
|
210
|
+
* **onResponseStart** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string []>, statusMessage?: string) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
|
|
211
211
|
* **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
|
|
212
212
|
* **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
|
|
213
213
|
* **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
|
|
@@ -1054,203 +1054,27 @@ const response = await client.request({
|
|
|
1054
1054
|
})
|
|
1055
1055
|
```
|
|
1056
1056
|
|
|
1057
|
-
##### `
|
|
1057
|
+
##### `responseError`
|
|
1058
1058
|
|
|
1059
|
-
|
|
1059
|
+
The `responseError` interceptor throws an error for responses with status code errors (>= 400).
|
|
1060
1060
|
|
|
1061
|
-
|
|
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**
|
|
1185
1062
|
|
|
1186
1063
|
```js
|
|
1187
|
-
|
|
1188
|
-
|
|
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;
|
|
1227
1066
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
+
);
|
|
1245
1070
|
|
|
1246
|
-
|
|
1071
|
+
// Will throw a ResponseError for status codes >= 400
|
|
1072
|
+
await client.request({
|
|
1073
|
+
method: "GET",
|
|
1074
|
+
path: "/"
|
|
1247
1075
|
});
|
|
1248
1076
|
```
|
|
1249
1077
|
|
|
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
|
-
|
|
1254
1078
|
##### `Cache Interceptor`
|
|
1255
1079
|
|
|
1256
1080
|
The `cache` interceptor implements client-side response caching as described in
|
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ module.exports.DecoratorHandler = DecoratorHandler
|
|
|
38
38
|
module.exports.RedirectHandler = RedirectHandler
|
|
39
39
|
module.exports.interceptors = {
|
|
40
40
|
redirect: require('./lib/interceptor/redirect'),
|
|
41
|
+
responseError: require('./lib/interceptor/response-error'),
|
|
41
42
|
retry: require('./lib/interceptor/retry'),
|
|
42
43
|
dump: require('./lib/interceptor/dump'),
|
|
43
44
|
dns: require('./lib/interceptor/dns'),
|
package/lib/core/errors.js
CHANGED
|
@@ -196,13 +196,13 @@ class RequestRetryError extends UndiciError {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
class ResponseError extends UndiciError {
|
|
199
|
-
constructor (message, code, { headers,
|
|
199
|
+
constructor (message, code, { headers, body }) {
|
|
200
200
|
super(message)
|
|
201
201
|
this.name = 'ResponseError'
|
|
202
202
|
this.message = message || 'Response error'
|
|
203
203
|
this.code = 'UND_ERR_RESPONSE'
|
|
204
204
|
this.statusCode = code
|
|
205
|
-
this.
|
|
205
|
+
this.body = body
|
|
206
206
|
this.headers = headers
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -130,6 +130,8 @@ class DispatcherBase extends Dispatcher {
|
|
|
130
130
|
throw new InvalidArgumentError('handler must be an object')
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
handler = UnwrapHandler.unwrap(handler)
|
|
134
|
+
|
|
133
135
|
try {
|
|
134
136
|
if (!opts || typeof opts !== 'object') {
|
|
135
137
|
throw new InvalidArgumentError('opts must be an object.')
|
|
@@ -143,10 +145,10 @@ class DispatcherBase extends Dispatcher {
|
|
|
143
145
|
throw new ClientClosedError()
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
return this[kDispatch](opts,
|
|
148
|
+
return this[kDispatch](opts, handler)
|
|
147
149
|
} catch (err) {
|
|
148
150
|
if (typeof handler.onError !== 'function') {
|
|
149
|
-
throw
|
|
151
|
+
throw err
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
handler.onError(err)
|
|
@@ -71,15 +71,15 @@ class CacheHandler {
|
|
|
71
71
|
onResponseStart (
|
|
72
72
|
controller,
|
|
73
73
|
statusCode,
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
headers,
|
|
75
|
+
statusMessage
|
|
76
76
|
) {
|
|
77
77
|
const downstreamOnHeaders = () =>
|
|
78
78
|
this.#handler.onResponseStart?.(
|
|
79
79
|
controller,
|
|
80
80
|
statusCode,
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
headers,
|
|
82
|
+
statusMessage
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
if (
|
|
@@ -62,8 +62,8 @@ class CacheRevalidationHandler {
|
|
|
62
62
|
onResponseStart (
|
|
63
63
|
controller,
|
|
64
64
|
statusCode,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
headers,
|
|
66
|
+
statusMessage
|
|
67
67
|
) {
|
|
68
68
|
assert(this.#callback != null)
|
|
69
69
|
|
|
@@ -82,8 +82,8 @@ class CacheRevalidationHandler {
|
|
|
82
82
|
this.#handler.onResponseStart?.(
|
|
83
83
|
controller,
|
|
84
84
|
statusCode,
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
headers,
|
|
86
|
+
statusMessage
|
|
87
87
|
)
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -92,7 +92,7 @@ class CacheRevalidationHandler {
|
|
|
92
92
|
return
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
return this.#handler.onResponseData(controller, chunk)
|
|
95
|
+
return this.#handler.onResponseData?.(controller, chunk)
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
onResponseEnd (controller, trailers) {
|
|
@@ -90,7 +90,7 @@ class RedirectHandler {
|
|
|
90
90
|
this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
onResponseStart (controller, statusCode,
|
|
93
|
+
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
94
94
|
if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
|
|
95
95
|
throw new Error('max redirects')
|
|
96
96
|
}
|
|
@@ -125,7 +125,7 @@ class RedirectHandler {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
if (!this.location) {
|
|
128
|
-
this.handler.onResponseStart?.(controller, statusCode,
|
|
128
|
+
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
|
129
129
|
return
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -3,9 +3,9 @@ const assert = require('node:assert')
|
|
|
3
3
|
|
|
4
4
|
const { kRetryHandlerDefaultRetry } = require('../core/symbols')
|
|
5
5
|
const { RequestRetryError } = require('../core/errors')
|
|
6
|
+
const WrapHandler = require('./wrap-handler')
|
|
6
7
|
const {
|
|
7
8
|
isDisturbed,
|
|
8
|
-
parseHeaders,
|
|
9
9
|
parseRangeHeader,
|
|
10
10
|
wrapRequestBody
|
|
11
11
|
} = require('../core/util')
|
|
@@ -16,7 +16,7 @@ function calculateRetryAfterHeader (retryAfter) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
class RetryHandler {
|
|
19
|
-
constructor (opts,
|
|
19
|
+
constructor (opts, { dispatch, handler }) {
|
|
20
20
|
const { retryOptions, ...dispatchOpts } = opts
|
|
21
21
|
const {
|
|
22
22
|
// Retry scoped
|
|
@@ -32,12 +32,9 @@ class RetryHandler {
|
|
|
32
32
|
statusCodes
|
|
33
33
|
} = retryOptions ?? {}
|
|
34
34
|
|
|
35
|
-
this.dispatch =
|
|
36
|
-
this.handler =
|
|
35
|
+
this.dispatch = dispatch
|
|
36
|
+
this.handler = WrapHandler.wrap(handler)
|
|
37
37
|
this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
|
|
38
|
-
this.abort = null
|
|
39
|
-
this.aborted = false
|
|
40
|
-
this.connectCalled = false
|
|
41
38
|
this.retryOpts = {
|
|
42
39
|
retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
|
|
43
40
|
retryAfter: retryAfter ?? true,
|
|
@@ -65,37 +62,20 @@ class RetryHandler {
|
|
|
65
62
|
|
|
66
63
|
this.retryCount = 0
|
|
67
64
|
this.retryCountCheckpoint = 0
|
|
65
|
+
this.headersSent = false
|
|
68
66
|
this.start = 0
|
|
69
67
|
this.end = null
|
|
70
68
|
this.etag = null
|
|
71
|
-
this.resume = null
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
if (this.
|
|
76
|
-
this.handler.
|
|
71
|
+
onRequestStart (controller, context) {
|
|
72
|
+
if (!this.headersSent) {
|
|
73
|
+
this.handler.onRequestStart?.(controller, context)
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.handler.onUpgrade(statusCode, headers, socket)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
onConnect (abort, context) {
|
|
87
|
-
this.abort = abort
|
|
88
|
-
if (!this.connectCalled) {
|
|
89
|
-
this.connectCalled = true
|
|
90
|
-
this.handler.onConnect(reason => {
|
|
91
|
-
this.aborted = true
|
|
92
|
-
this.abort(reason)
|
|
93
|
-
}, context)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
onBodySent (chunk) {
|
|
98
|
-
if (this.handler.onBodySent) return this.handler.onBodySent(chunk)
|
|
77
|
+
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
78
|
+
this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
|
|
99
79
|
}
|
|
100
80
|
|
|
101
81
|
static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
|
|
@@ -153,83 +133,68 @@ class RetryHandler {
|
|
|
153
133
|
? Math.min(retryAfterHeader, maxTimeout)
|
|
154
134
|
: Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
|
|
155
135
|
|
|
156
|
-
setTimeout(() => cb(null), retryTimeout)
|
|
136
|
+
setTimeout(() => cb(null), retryTimeout).unref()
|
|
157
137
|
}
|
|
158
138
|
|
|
159
|
-
|
|
160
|
-
const headers = parseHeaders(rawHeaders)
|
|
161
|
-
|
|
139
|
+
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
162
140
|
this.retryCount += 1
|
|
163
141
|
|
|
164
142
|
if (statusCode >= 300) {
|
|
165
143
|
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
|
166
|
-
|
|
144
|
+
this.headersSent = true
|
|
145
|
+
this.handler.onResponseStart?.(
|
|
146
|
+
controller,
|
|
167
147
|
statusCode,
|
|
168
|
-
|
|
169
|
-
resume,
|
|
148
|
+
headers,
|
|
170
149
|
statusMessage
|
|
171
150
|
)
|
|
151
|
+
return
|
|
172
152
|
} else {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
})
|
|
180
|
-
)
|
|
181
|
-
return false
|
|
153
|
+
throw new RequestRetryError('Request failed', statusCode, {
|
|
154
|
+
headers,
|
|
155
|
+
data: {
|
|
156
|
+
count: this.retryCount
|
|
157
|
+
}
|
|
158
|
+
})
|
|
182
159
|
}
|
|
183
160
|
}
|
|
184
161
|
|
|
185
162
|
// Checkpoint for resume from where we left it
|
|
186
|
-
if (this.
|
|
187
|
-
this.resume = null
|
|
188
|
-
|
|
163
|
+
if (this.headersSent) {
|
|
189
164
|
// Only Partial Content 206 supposed to provide Content-Range,
|
|
190
165
|
// any other status code that partially consumed the payload
|
|
191
166
|
// should not be retried because it would result in downstream
|
|
192
167
|
// wrongly concatenate multiple responses.
|
|
193
168
|
if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
199
|
-
)
|
|
200
|
-
return false
|
|
169
|
+
throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
|
|
170
|
+
headers,
|
|
171
|
+
data: { count: this.retryCount }
|
|
172
|
+
})
|
|
201
173
|
}
|
|
202
174
|
|
|
203
175
|
const contentRange = parseRangeHeader(headers['content-range'])
|
|
204
176
|
// If no content range
|
|
205
177
|
if (!contentRange) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
})
|
|
211
|
-
)
|
|
212
|
-
return false
|
|
178
|
+
throw new RequestRetryError('Content-Range mismatch', statusCode, {
|
|
179
|
+
headers,
|
|
180
|
+
data: { count: this.retryCount }
|
|
181
|
+
})
|
|
213
182
|
}
|
|
214
183
|
|
|
215
184
|
// Let's start with a weak etag check
|
|
216
185
|
if (this.etag != null && this.etag !== headers.etag) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
})
|
|
222
|
-
)
|
|
223
|
-
return false
|
|
186
|
+
throw new RequestRetryError('ETag mismatch', statusCode, {
|
|
187
|
+
headers,
|
|
188
|
+
data: { count: this.retryCount }
|
|
189
|
+
})
|
|
224
190
|
}
|
|
225
191
|
|
|
226
|
-
const { start, size, end = size - 1 } = contentRange
|
|
192
|
+
const { start, size, end = size ? size - 1 : null } = contentRange
|
|
227
193
|
|
|
228
194
|
assert(this.start === start, 'content-range mismatch')
|
|
229
195
|
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
230
196
|
|
|
231
|
-
|
|
232
|
-
return true
|
|
197
|
+
return
|
|
233
198
|
}
|
|
234
199
|
|
|
235
200
|
if (this.end == null) {
|
|
@@ -238,15 +203,17 @@ class RetryHandler {
|
|
|
238
203
|
const range = parseRangeHeader(headers['content-range'])
|
|
239
204
|
|
|
240
205
|
if (range == null) {
|
|
241
|
-
|
|
206
|
+
this.headersSent = true
|
|
207
|
+
this.handler.onResponseStart?.(
|
|
208
|
+
controller,
|
|
242
209
|
statusCode,
|
|
243
|
-
|
|
244
|
-
resume,
|
|
210
|
+
headers,
|
|
245
211
|
statusMessage
|
|
246
212
|
)
|
|
213
|
+
return
|
|
247
214
|
}
|
|
248
215
|
|
|
249
|
-
const { start, size, end = size - 1 } = range
|
|
216
|
+
const { start, size, end = size ? size - 1 : null } = range
|
|
250
217
|
assert(
|
|
251
218
|
start != null && Number.isFinite(start),
|
|
252
219
|
'content-range mismatch'
|
|
@@ -269,7 +236,7 @@ class RetryHandler {
|
|
|
269
236
|
'invalid content-length'
|
|
270
237
|
)
|
|
271
238
|
|
|
272
|
-
this.resume =
|
|
239
|
+
this.resume = true
|
|
273
240
|
this.etag = headers.etag != null ? headers.etag : null
|
|
274
241
|
|
|
275
242
|
// Weak etags are not useful for comparison nor cache
|
|
@@ -283,38 +250,36 @@ class RetryHandler {
|
|
|
283
250
|
this.etag = null
|
|
284
251
|
}
|
|
285
252
|
|
|
286
|
-
|
|
253
|
+
this.headersSent = true
|
|
254
|
+
this.handler.onResponseStart?.(
|
|
255
|
+
controller,
|
|
287
256
|
statusCode,
|
|
288
|
-
|
|
289
|
-
resume,
|
|
257
|
+
headers,
|
|
290
258
|
statusMessage
|
|
291
259
|
)
|
|
260
|
+
} else {
|
|
261
|
+
throw new RequestRetryError('Request failed', statusCode, {
|
|
262
|
+
headers,
|
|
263
|
+
data: { count: this.retryCount }
|
|
264
|
+
})
|
|
292
265
|
}
|
|
293
|
-
|
|
294
|
-
const err = new RequestRetryError('Request failed', statusCode, {
|
|
295
|
-
headers,
|
|
296
|
-
data: { count: this.retryCount }
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
this.abort(err)
|
|
300
|
-
|
|
301
|
-
return false
|
|
302
266
|
}
|
|
303
267
|
|
|
304
|
-
|
|
268
|
+
onResponseData (controller, chunk) {
|
|
305
269
|
this.start += chunk.length
|
|
306
270
|
|
|
307
|
-
|
|
271
|
+
this.handler.onResponseData?.(controller, chunk)
|
|
308
272
|
}
|
|
309
273
|
|
|
310
|
-
|
|
274
|
+
onResponseEnd (controller, trailers) {
|
|
311
275
|
this.retryCount = 0
|
|
312
|
-
return this.handler.
|
|
276
|
+
return this.handler.onResponseEnd?.(controller, trailers)
|
|
313
277
|
}
|
|
314
278
|
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
279
|
+
onResponseError (controller, err) {
|
|
280
|
+
if (!controller || controller.aborted || isDisturbed(this.opts.body)) {
|
|
281
|
+
this.handler.onResponseError?.(controller, err)
|
|
282
|
+
return
|
|
318
283
|
}
|
|
319
284
|
|
|
320
285
|
// We reconcile in case of a mix between network errors
|
|
@@ -343,8 +308,8 @@ class RetryHandler {
|
|
|
343
308
|
* @returns
|
|
344
309
|
*/
|
|
345
310
|
function onRetry (err) {
|
|
346
|
-
if (err != null ||
|
|
347
|
-
return this.handler.
|
|
311
|
+
if (err != null || controller?.aborted || isDisturbed(this.opts.body)) {
|
|
312
|
+
return this.handler.onResponseError?.(controller, err)
|
|
348
313
|
}
|
|
349
314
|
|
|
350
315
|
if (this.start !== 0) {
|
|
@@ -368,7 +333,7 @@ class RetryHandler {
|
|
|
368
333
|
this.retryCountCheckpoint = this.retryCount
|
|
369
334
|
this.dispatch(this.opts, this)
|
|
370
335
|
} catch (err) {
|
|
371
|
-
this.handler.
|
|
336
|
+
this.handler.onResponseError?.(controller, err)
|
|
372
337
|
}
|
|
373
338
|
}
|
|
374
339
|
}
|
|
@@ -73,7 +73,7 @@ module.exports = class UnwrapHandler {
|
|
|
73
73
|
|
|
74
74
|
onHeaders (statusCode, rawHeaders, resume, statusMessage) {
|
|
75
75
|
this.#controller[kResume] = resume
|
|
76
|
-
this.#handler.onResponseStart?.(this.#controller, statusCode,
|
|
76
|
+
this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage)
|
|
77
77
|
return !this.#controller.paused
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -38,7 +38,7 @@ module.exports = class WrapHandler {
|
|
|
38
38
|
|
|
39
39
|
onError (err) {
|
|
40
40
|
if (!this.#handler.onError) {
|
|
41
|
-
throw
|
|
41
|
+
throw err
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
return this.#handler.onError?.(err)
|
|
@@ -60,7 +60,7 @@ module.exports = class WrapHandler {
|
|
|
60
60
|
this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
onResponseStart (controller, statusCode,
|
|
63
|
+
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
64
64
|
const rawHeaders = []
|
|
65
65
|
for (const [key, val] of Object.entries(headers)) {
|
|
66
66
|
// TODO (fix): What if val is Array
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -163,7 +163,7 @@ function sendCachedValue (handler, opts, result, age, context) {
|
|
|
163
163
|
// TODO (fix): What if headers.age already exists?
|
|
164
164
|
const headers = age != null ? { ...result.headers, age: String(age) } : result.headers
|
|
165
165
|
|
|
166
|
-
handler.onResponseStart?.(controller, result.statusCode, result.statusMessage
|
|
166
|
+
handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
|
|
167
167
|
|
|
168
168
|
if (opts.method === 'HEAD') {
|
|
169
169
|
stream.destroy()
|
|
@@ -195,9 +195,6 @@ function handleResult (
|
|
|
195
195
|
if (!result) {
|
|
196
196
|
return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
|
|
197
197
|
}
|
|
198
|
-
if (!result.body && opts.method !== 'HEAD') {
|
|
199
|
-
throw new Error('body is undefined but method isn\'t HEAD')
|
|
200
|
-
}
|
|
201
198
|
|
|
202
199
|
const now = Date.now()
|
|
203
200
|
if (now > result.deleteAt) {
|
|
@@ -4,7 +4,7 @@ const { parseHeaders } = require('../core/util')
|
|
|
4
4
|
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
5
|
const { ResponseError } = require('../core/errors')
|
|
6
6
|
|
|
7
|
-
class
|
|
7
|
+
class ResponseErrorHandler extends DecoratorHandler {
|
|
8
8
|
#handler
|
|
9
9
|
#statusCode
|
|
10
10
|
#contentType
|
|
@@ -66,7 +66,7 @@ class Handler extends DecoratorHandler {
|
|
|
66
66
|
Error.stackTraceLimit = 0
|
|
67
67
|
try {
|
|
68
68
|
err = new ResponseError('Response Error', this.#statusCode, {
|
|
69
|
-
|
|
69
|
+
body: this.#body,
|
|
70
70
|
headers: this.#headers
|
|
71
71
|
})
|
|
72
72
|
} finally {
|
|
@@ -84,6 +84,10 @@ class Handler extends DecoratorHandler {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
module.exports = (
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
module.exports = () => {
|
|
88
|
+
return (dispatch) => {
|
|
89
|
+
return function Intercept (opts, handler) {
|
|
90
|
+
return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/package.json
CHANGED
package/types/dispatcher.d.ts
CHANGED
|
@@ -226,7 +226,7 @@ declare namespace Dispatcher {
|
|
|
226
226
|
export interface DispatchHandler {
|
|
227
227
|
onRequestStart?(controller: DispatchController, context: any): void;
|
|
228
228
|
onRequestUpgrade?(controller: DispatchController, statusCode: number, headers: IncomingHttpHeaders, socket: Duplex): void;
|
|
229
|
-
onResponseStart?(controller: DispatchController, statusCode: number,
|
|
229
|
+
onResponseStart?(controller: DispatchController, statusCode: number, headers: IncomingHttpHeaders, statusMessage?: string): void;
|
|
230
230
|
onResponseData?(controller: DispatchController, chunk: Buffer): void;
|
|
231
231
|
onResponseEnd?(controller: DispatchController, trailers: IncomingHttpHeaders): void;
|
|
232
232
|
onResponseError?(controller: DispatchController, error: Error): void;
|