undici 6.19.8 → 6.20.0
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 +197 -0
- package/docs/docs/api/RetryHandler.md +1 -1
- package/lib/api/api-upgrade.js +2 -2
- package/lib/core/connect.js +71 -29
- package/lib/core/errors.js +13 -0
- package/lib/core/util.js +1 -1
- package/lib/dispatcher/client-h1.js +62 -42
- package/lib/dispatcher/client.js +4 -0
- package/lib/handler/retry-handler.js +12 -2
- package/lib/interceptor/response-error.js +86 -0
- package/lib/mock/mock-utils.js +4 -0
- package/lib/util/timers.js +367 -43
- package/lib/web/fetch/formdata-parser.js +12 -2
- package/lib/web/fetch/index.js +16 -4
- package/lib/web/fetch/util.js +16 -4
- package/package.json +2 -2
- package/types/eventsource.d.ts +0 -2
- package/types/filereader.d.ts +1 -1
- package/types/interceptors.d.ts +3 -1
- package/types/patch.d.ts +0 -38
- package/types/websocket.d.ts +0 -2
|
@@ -984,6 +984,203 @@ client.dispatch(
|
|
|
984
984
|
);
|
|
985
985
|
```
|
|
986
986
|
|
|
987
|
+
##### `Response Error Interceptor`
|
|
988
|
+
|
|
989
|
+
**Introduction**
|
|
990
|
+
|
|
991
|
+
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.
|
|
992
|
+
|
|
993
|
+
**ResponseError Class**
|
|
994
|
+
|
|
995
|
+
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.
|
|
996
|
+
|
|
997
|
+
**Definition**
|
|
998
|
+
|
|
999
|
+
```js
|
|
1000
|
+
class ResponseError extends UndiciError {
|
|
1001
|
+
constructor (message, code, { headers, data }) {
|
|
1002
|
+
super(message);
|
|
1003
|
+
this.name = 'ResponseError';
|
|
1004
|
+
this.message = message || 'Response error';
|
|
1005
|
+
this.code = 'UND_ERR_RESPONSE';
|
|
1006
|
+
this.statusCode = code;
|
|
1007
|
+
this.data = data;
|
|
1008
|
+
this.headers = headers;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Interceptor Handler**
|
|
1014
|
+
|
|
1015
|
+
The interceptor's handler class extends `DecoratorHandler` and overrides methods to capture response details and handle errors based on the response status code.
|
|
1016
|
+
|
|
1017
|
+
**Methods**
|
|
1018
|
+
|
|
1019
|
+
- **onConnect**: Initializes response properties.
|
|
1020
|
+
- **onHeaders**: Captures headers and status code. Decodes body if content type is `application/json` or `text/plain`.
|
|
1021
|
+
- **onData**: Appends chunks to the body if status code indicates an error.
|
|
1022
|
+
- **onComplete**: Finalizes error handling, constructs a `ResponseError`, and invokes the `onError` method.
|
|
1023
|
+
- **onError**: Propagates errors to the handler.
|
|
1024
|
+
|
|
1025
|
+
**Definition**
|
|
1026
|
+
|
|
1027
|
+
```js
|
|
1028
|
+
class Handler extends DecoratorHandler {
|
|
1029
|
+
// Private properties
|
|
1030
|
+
#handler;
|
|
1031
|
+
#statusCode;
|
|
1032
|
+
#contentType;
|
|
1033
|
+
#decoder;
|
|
1034
|
+
#headers;
|
|
1035
|
+
#body;
|
|
1036
|
+
|
|
1037
|
+
constructor (opts, { handler }) {
|
|
1038
|
+
super(handler);
|
|
1039
|
+
this.#handler = handler;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
onConnect (abort) {
|
|
1043
|
+
this.#statusCode = 0;
|
|
1044
|
+
this.#contentType = null;
|
|
1045
|
+
this.#decoder = null;
|
|
1046
|
+
this.#headers = null;
|
|
1047
|
+
this.#body = '';
|
|
1048
|
+
return this.#handler.onConnect(abort);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
|
|
1052
|
+
this.#statusCode = statusCode;
|
|
1053
|
+
this.#headers = headers;
|
|
1054
|
+
this.#contentType = headers['content-type'];
|
|
1055
|
+
|
|
1056
|
+
if (this.#statusCode < 400) {
|
|
1057
|
+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
|
|
1061
|
+
this.#decoder = new TextDecoder('utf-8');
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
onData (chunk) {
|
|
1066
|
+
if (this.#statusCode < 400) {
|
|
1067
|
+
return this.#handler.onData(chunk);
|
|
1068
|
+
}
|
|
1069
|
+
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '';
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
onComplete (rawTrailers) {
|
|
1073
|
+
if (this.#statusCode >= 400) {
|
|
1074
|
+
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '';
|
|
1075
|
+
if (this.#contentType === 'application/json') {
|
|
1076
|
+
try {
|
|
1077
|
+
this.#body = JSON.parse(this.#body);
|
|
1078
|
+
} catch {
|
|
1079
|
+
// Do nothing...
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
let err;
|
|
1084
|
+
const stackTraceLimit = Error.stackTraceLimit;
|
|
1085
|
+
Error.stackTraceLimit = 0;
|
|
1086
|
+
try {
|
|
1087
|
+
err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body);
|
|
1088
|
+
} finally {
|
|
1089
|
+
Error.stackTraceLimit = stackTraceLimit;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.#handler.onError(err);
|
|
1093
|
+
} else {
|
|
1094
|
+
this.#handler.onComplete(rawTrailers);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
onError (err) {
|
|
1099
|
+
this.#handler.onError(err);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
module.exports = (dispatch) => (opts, handler) => opts.throwOnError
|
|
1104
|
+
? dispatch(opts, new Handler(opts, { handler }))
|
|
1105
|
+
: dispatch(opts, handler);
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
**Tests**
|
|
1109
|
+
|
|
1110
|
+
Unit tests ensure the interceptor functions correctly, handling both error and non-error responses appropriately.
|
|
1111
|
+
|
|
1112
|
+
**Example Tests**
|
|
1113
|
+
|
|
1114
|
+
- **No Error if `throwOnError` is False**:
|
|
1115
|
+
|
|
1116
|
+
```js
|
|
1117
|
+
test('should not error if request is not meant to throw error', async (t) => {
|
|
1118
|
+
const opts = { throwOnError: false };
|
|
1119
|
+
const handler = { onError: () => {}, onData: () => {}, onComplete: () => {} };
|
|
1120
|
+
const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete());
|
|
1121
|
+
assert.doesNotThrow(() => interceptor(opts, handler));
|
|
1122
|
+
});
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
- **Error if Status Code is in Specified Error Codes**:
|
|
1126
|
+
|
|
1127
|
+
```js
|
|
1128
|
+
test('should error if request status code is in the specified error codes', async (t) => {
|
|
1129
|
+
const opts = { throwOnError: true, statusCodes: [500] };
|
|
1130
|
+
const response = { statusCode: 500 };
|
|
1131
|
+
let capturedError;
|
|
1132
|
+
const handler = {
|
|
1133
|
+
onError: (err) => { capturedError = err; },
|
|
1134
|
+
onData: () => {},
|
|
1135
|
+
onComplete: () => {}
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
const interceptor = createResponseErrorInterceptor((opts, handler) => {
|
|
1139
|
+
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
|
|
1140
|
+
handler.onError(new Error('Response Error'));
|
|
1141
|
+
} else {
|
|
1142
|
+
handler.onComplete();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
interceptor({ ...opts, response }, handler);
|
|
1147
|
+
|
|
1148
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
1149
|
+
|
|
1150
|
+
assert(capturedError, 'Expected error to be captured but it was not.');
|
|
1151
|
+
assert.strictEqual(capturedError.message, 'Response Error');
|
|
1152
|
+
assert.strictEqual(response.statusCode, 500);
|
|
1153
|
+
});
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
- **No Error if Status Code is Not in Specified Error Codes**:
|
|
1157
|
+
|
|
1158
|
+
```js
|
|
1159
|
+
test('should not error if request status code is not in the specified error codes', async (t) => {
|
|
1160
|
+
const opts = { throwOnError: true, statusCodes: [500] };
|
|
1161
|
+
const response = { statusCode: 404 };
|
|
1162
|
+
const handler = {
|
|
1163
|
+
onError: () => {},
|
|
1164
|
+
onData: () => {},
|
|
1165
|
+
onComplete: () => {}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const interceptor = createResponseErrorInterceptor((opts, handler) => {
|
|
1169
|
+
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
|
|
1170
|
+
handler.onError(new Error('Response Error'));
|
|
1171
|
+
} else {
|
|
1172
|
+
handler.onComplete();
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
assert.doesNotThrow(() => interceptor({ ...opts, response }, handler));
|
|
1177
|
+
});
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
**Conclusion**
|
|
1181
|
+
|
|
1182
|
+
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.
|
|
1183
|
+
|
|
987
1184
|
## Instance Events
|
|
988
1185
|
|
|
989
1186
|
### Event: `'connect'`
|
|
@@ -19,7 +19,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
|
|
|
19
19
|
|
|
20
20
|
#### `RetryOptions`
|
|
21
21
|
|
|
22
|
-
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) =>
|
|
22
|
+
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => number | null` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
|
|
23
23
|
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
|
|
24
24
|
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
|
|
25
25
|
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
|
package/lib/api/api-upgrade.js
CHANGED
|
@@ -50,9 +50,9 @@ class UpgradeHandler extends AsyncResource {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
onUpgrade (statusCode, rawHeaders, socket) {
|
|
53
|
-
|
|
53
|
+
assert(statusCode === 101)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const { callback, opaque, context } = this
|
|
56
56
|
|
|
57
57
|
removeSignal(this)
|
|
58
58
|
|
package/lib/core/connect.js
CHANGED
|
@@ -4,6 +4,9 @@ const net = require('node:net')
|
|
|
4
4
|
const assert = require('node:assert')
|
|
5
5
|
const util = require('./util')
|
|
6
6
|
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
|
7
|
+
const timers = require('../util/timers')
|
|
8
|
+
|
|
9
|
+
function noop () {}
|
|
7
10
|
|
|
8
11
|
let tls // include tls conditionally since it is not always available
|
|
9
12
|
|
|
@@ -91,9 +94,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
91
94
|
servername = servername || options.servername || util.getServerName(host) || null
|
|
92
95
|
|
|
93
96
|
const sessionKey = servername || hostname
|
|
97
|
+
assert(sessionKey)
|
|
98
|
+
|
|
94
99
|
const session = customSession || sessionCache.get(sessionKey) || null
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
port = port || 443
|
|
97
102
|
|
|
98
103
|
socket = tls.connect({
|
|
99
104
|
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
|
|
@@ -104,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
104
109
|
// TODO(HTTP/2): Add support for h2c
|
|
105
110
|
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
|
|
106
111
|
socket: httpSocket, // upgrade socket connection
|
|
107
|
-
port
|
|
112
|
+
port,
|
|
108
113
|
host: hostname
|
|
109
114
|
})
|
|
110
115
|
|
|
@@ -115,11 +120,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
115
120
|
})
|
|
116
121
|
} else {
|
|
117
122
|
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
|
|
123
|
+
|
|
124
|
+
port = port || 80
|
|
125
|
+
|
|
118
126
|
socket = net.connect({
|
|
119
127
|
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
|
|
120
128
|
...options,
|
|
121
129
|
localAddress,
|
|
122
|
-
port
|
|
130
|
+
port,
|
|
123
131
|
host: hostname
|
|
124
132
|
})
|
|
125
133
|
}
|
|
@@ -130,12 +138,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
130
138
|
socket.setKeepAlive(true, keepAliveInitialDelay)
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
const
|
|
141
|
+
const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
|
|
134
142
|
|
|
135
143
|
socket
|
|
136
144
|
.setNoDelay(true)
|
|
137
145
|
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
|
|
138
|
-
|
|
146
|
+
queueMicrotask(clearConnectTimeout)
|
|
139
147
|
|
|
140
148
|
if (callback) {
|
|
141
149
|
const cb = callback
|
|
@@ -144,7 +152,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
144
152
|
}
|
|
145
153
|
})
|
|
146
154
|
.on('error', function (err) {
|
|
147
|
-
|
|
155
|
+
queueMicrotask(clearConnectTimeout)
|
|
148
156
|
|
|
149
157
|
if (callback) {
|
|
150
158
|
const cb = callback
|
|
@@ -157,36 +165,70 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
|
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
/**
|
|
169
|
+
* @param {WeakRef<net.Socket>} socketWeakRef
|
|
170
|
+
* @param {object} opts
|
|
171
|
+
* @param {number} opts.timeout
|
|
172
|
+
* @param {string} opts.hostname
|
|
173
|
+
* @param {number} opts.port
|
|
174
|
+
* @returns {() => void}
|
|
175
|
+
*/
|
|
176
|
+
const setupConnectTimeout = process.platform === 'win32'
|
|
177
|
+
? (socketWeakRef, opts) => {
|
|
178
|
+
if (!opts.timeout) {
|
|
179
|
+
return noop
|
|
180
|
+
}
|
|
164
181
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (process.platform === 'win32') {
|
|
182
|
+
let s1 = null
|
|
183
|
+
let s2 = null
|
|
184
|
+
const fastTimer = timers.setFastTimeout(() => {
|
|
185
|
+
// setImmediate is added to make sure that we prioritize socket error events over timeouts
|
|
186
|
+
s1 = setImmediate(() => {
|
|
171
187
|
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
|
|
189
|
+
})
|
|
190
|
+
}, opts.timeout)
|
|
191
|
+
return () => {
|
|
192
|
+
timers.clearFastTimeout(fastTimer)
|
|
193
|
+
clearImmediate(s1)
|
|
194
|
+
clearImmediate(s2)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
: (socketWeakRef, opts) => {
|
|
198
|
+
if (!opts.timeout) {
|
|
199
|
+
return noop
|
|
175
200
|
}
|
|
176
|
-
})
|
|
177
|
-
}, timeout)
|
|
178
|
-
return () => {
|
|
179
|
-
clearTimeout(timeoutId)
|
|
180
|
-
clearImmediate(s1)
|
|
181
|
-
clearImmediate(s2)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
201
|
|
|
185
|
-
|
|
202
|
+
let s1 = null
|
|
203
|
+
const fastTimer = timers.setFastTimeout(() => {
|
|
204
|
+
// setImmediate is added to make sure that we prioritize socket error events over timeouts
|
|
205
|
+
s1 = setImmediate(() => {
|
|
206
|
+
onConnectTimeout(socketWeakRef.deref(), opts)
|
|
207
|
+
})
|
|
208
|
+
}, opts.timeout)
|
|
209
|
+
return () => {
|
|
210
|
+
timers.clearFastTimeout(fastTimer)
|
|
211
|
+
clearImmediate(s1)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {net.Socket} socket
|
|
217
|
+
* @param {object} opts
|
|
218
|
+
* @param {number} opts.timeout
|
|
219
|
+
* @param {string} opts.hostname
|
|
220
|
+
* @param {number} opts.port
|
|
221
|
+
*/
|
|
222
|
+
function onConnectTimeout (socket, opts) {
|
|
186
223
|
let message = 'Connect Timeout Error'
|
|
187
224
|
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
|
|
188
|
-
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')}
|
|
225
|
+
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
|
|
226
|
+
} else {
|
|
227
|
+
message += ` (attempted address: ${opts.hostname}:${opts.port},`
|
|
189
228
|
}
|
|
229
|
+
|
|
230
|
+
message += ` timeout: ${opts.timeout}ms)`
|
|
231
|
+
|
|
190
232
|
util.destroy(socket, new ConnectTimeoutError(message))
|
|
191
233
|
}
|
|
192
234
|
|
package/lib/core/errors.js
CHANGED
|
@@ -195,6 +195,18 @@ class RequestRetryError extends UndiciError {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
class ResponseError extends UndiciError {
|
|
199
|
+
constructor (message, code, { headers, data }) {
|
|
200
|
+
super(message)
|
|
201
|
+
this.name = 'ResponseError'
|
|
202
|
+
this.message = message || 'Response error'
|
|
203
|
+
this.code = 'UND_ERR_RESPONSE'
|
|
204
|
+
this.statusCode = code
|
|
205
|
+
this.data = data
|
|
206
|
+
this.headers = headers
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
198
210
|
class SecureProxyConnectionError extends UndiciError {
|
|
199
211
|
constructor (cause, message, options) {
|
|
200
212
|
super(message, { cause, ...(options ?? {}) })
|
|
@@ -227,5 +239,6 @@ module.exports = {
|
|
|
227
239
|
BalancedPoolMissingUpstreamError,
|
|
228
240
|
ResponseExceededMaxSizeError,
|
|
229
241
|
RequestRetryError,
|
|
242
|
+
ResponseError,
|
|
230
243
|
SecureProxyConnectionError
|
|
231
244
|
}
|
package/lib/core/util.js
CHANGED
|
@@ -85,35 +85,35 @@ async function lazyllhttp () {
|
|
|
85
85
|
return 0
|
|
86
86
|
},
|
|
87
87
|
wasm_on_status: (p, at, len) => {
|
|
88
|
-
assert
|
|
88
|
+
assert(currentParser.ptr === p)
|
|
89
89
|
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
90
90
|
return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
91
91
|
},
|
|
92
92
|
wasm_on_message_begin: (p) => {
|
|
93
|
-
assert
|
|
93
|
+
assert(currentParser.ptr === p)
|
|
94
94
|
return currentParser.onMessageBegin() || 0
|
|
95
95
|
},
|
|
96
96
|
wasm_on_header_field: (p, at, len) => {
|
|
97
|
-
assert
|
|
97
|
+
assert(currentParser.ptr === p)
|
|
98
98
|
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
99
99
|
return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
100
100
|
},
|
|
101
101
|
wasm_on_header_value: (p, at, len) => {
|
|
102
|
-
assert
|
|
102
|
+
assert(currentParser.ptr === p)
|
|
103
103
|
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
104
104
|
return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
105
105
|
},
|
|
106
106
|
wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
|
|
107
|
-
assert
|
|
107
|
+
assert(currentParser.ptr === p)
|
|
108
108
|
return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0
|
|
109
109
|
},
|
|
110
110
|
wasm_on_body: (p, at, len) => {
|
|
111
|
-
assert
|
|
111
|
+
assert(currentParser.ptr === p)
|
|
112
112
|
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
113
113
|
return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
114
114
|
},
|
|
115
115
|
wasm_on_message_complete: (p) => {
|
|
116
|
-
assert
|
|
116
|
+
assert(currentParser.ptr === p)
|
|
117
117
|
return currentParser.onMessageComplete() || 0
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -131,9 +131,17 @@ let currentBufferRef = null
|
|
|
131
131
|
let currentBufferSize = 0
|
|
132
132
|
let currentBufferPtr = null
|
|
133
133
|
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
134
|
+
const USE_NATIVE_TIMER = 0
|
|
135
|
+
const USE_FAST_TIMER = 1
|
|
136
|
+
|
|
137
|
+
// Use fast timers for headers and body to take eventual event loop
|
|
138
|
+
// latency into account.
|
|
139
|
+
const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER
|
|
140
|
+
const TIMEOUT_BODY = 4 | USE_FAST_TIMER
|
|
141
|
+
|
|
142
|
+
// Use native timers to ignore event loop latency for keep-alive
|
|
143
|
+
// handling.
|
|
144
|
+
const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER
|
|
137
145
|
|
|
138
146
|
class Parser {
|
|
139
147
|
constructor (client, socket, { exports }) {
|
|
@@ -164,26 +172,39 @@ class Parser {
|
|
|
164
172
|
this.maxResponseSize = client[kMaxResponseSize]
|
|
165
173
|
}
|
|
166
174
|
|
|
167
|
-
setTimeout (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
setTimeout (delay, type) {
|
|
176
|
+
// If the existing timer and the new timer are of different timer type
|
|
177
|
+
// (fast or native) or have different delay, we need to clear the existing
|
|
178
|
+
// timer and set a new one.
|
|
179
|
+
if (
|
|
180
|
+
delay !== this.timeoutValue ||
|
|
181
|
+
(type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER)
|
|
182
|
+
) {
|
|
183
|
+
// If a timeout is already set, clear it with clearTimeout of the fast
|
|
184
|
+
// timer implementation, as it can clear fast and native timers.
|
|
185
|
+
if (this.timeout) {
|
|
186
|
+
timers.clearTimeout(this.timeout)
|
|
187
|
+
this.timeout = null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (delay) {
|
|
191
|
+
if (type & USE_FAST_TIMER) {
|
|
192
|
+
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
|
|
193
|
+
} else {
|
|
194
|
+
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
|
|
175
195
|
this.timeout.unref()
|
|
176
196
|
}
|
|
177
|
-
} else {
|
|
178
|
-
this.timeout = null
|
|
179
197
|
}
|
|
180
|
-
|
|
198
|
+
|
|
199
|
+
this.timeoutValue = delay
|
|
181
200
|
} else if (this.timeout) {
|
|
182
201
|
// istanbul ignore else: only for jest
|
|
183
202
|
if (this.timeout.refresh) {
|
|
184
203
|
this.timeout.refresh()
|
|
185
204
|
}
|
|
186
205
|
}
|
|
206
|
+
|
|
207
|
+
this.timeoutType = type
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
resume () {
|
|
@@ -288,7 +309,7 @@ class Parser {
|
|
|
288
309
|
this.llhttp.llhttp_free(this.ptr)
|
|
289
310
|
this.ptr = null
|
|
290
311
|
|
|
291
|
-
timers.clearTimeout(this.timeout)
|
|
312
|
+
this.timeout && timers.clearTimeout(this.timeout)
|
|
292
313
|
this.timeout = null
|
|
293
314
|
this.timeoutValue = null
|
|
294
315
|
this.timeoutType = null
|
|
@@ -363,20 +384,19 @@ class Parser {
|
|
|
363
384
|
const { upgrade, client, socket, headers, statusCode } = this
|
|
364
385
|
|
|
365
386
|
assert(upgrade)
|
|
387
|
+
assert(client[kSocket] === socket)
|
|
388
|
+
assert(!socket.destroyed)
|
|
389
|
+
assert(!this.paused)
|
|
390
|
+
assert((headers.length & 1) === 0)
|
|
366
391
|
|
|
367
392
|
const request = client[kQueue][client[kRunningIdx]]
|
|
368
393
|
assert(request)
|
|
369
|
-
|
|
370
|
-
assert(!socket.destroyed)
|
|
371
|
-
assert(socket === client[kSocket])
|
|
372
|
-
assert(!this.paused)
|
|
373
394
|
assert(request.upgrade || request.method === 'CONNECT')
|
|
374
395
|
|
|
375
396
|
this.statusCode = null
|
|
376
397
|
this.statusText = ''
|
|
377
398
|
this.shouldKeepAlive = null
|
|
378
399
|
|
|
379
|
-
assert(this.headers.length % 2 === 0)
|
|
380
400
|
this.headers = []
|
|
381
401
|
this.headersSize = 0
|
|
382
402
|
|
|
@@ -433,7 +453,7 @@ class Parser {
|
|
|
433
453
|
return -1
|
|
434
454
|
}
|
|
435
455
|
|
|
436
|
-
assert
|
|
456
|
+
assert(this.timeoutType === TIMEOUT_HEADERS)
|
|
437
457
|
|
|
438
458
|
this.statusCode = statusCode
|
|
439
459
|
this.shouldKeepAlive = (
|
|
@@ -466,7 +486,7 @@ class Parser {
|
|
|
466
486
|
return 2
|
|
467
487
|
}
|
|
468
488
|
|
|
469
|
-
assert(this.headers.length
|
|
489
|
+
assert((this.headers.length & 1) === 0)
|
|
470
490
|
this.headers = []
|
|
471
491
|
this.headersSize = 0
|
|
472
492
|
|
|
@@ -523,7 +543,7 @@ class Parser {
|
|
|
523
543
|
const request = client[kQueue][client[kRunningIdx]]
|
|
524
544
|
assert(request)
|
|
525
545
|
|
|
526
|
-
assert
|
|
546
|
+
assert(this.timeoutType === TIMEOUT_BODY)
|
|
527
547
|
if (this.timeout) {
|
|
528
548
|
// istanbul ignore else: only for jest
|
|
529
549
|
if (this.timeout.refresh) {
|
|
@@ -556,11 +576,12 @@ class Parser {
|
|
|
556
576
|
return
|
|
557
577
|
}
|
|
558
578
|
|
|
579
|
+
assert(statusCode >= 100)
|
|
580
|
+
assert((this.headers.length & 1) === 0)
|
|
581
|
+
|
|
559
582
|
const request = client[kQueue][client[kRunningIdx]]
|
|
560
583
|
assert(request)
|
|
561
584
|
|
|
562
|
-
assert(statusCode >= 100)
|
|
563
|
-
|
|
564
585
|
this.statusCode = null
|
|
565
586
|
this.statusText = ''
|
|
566
587
|
this.bytesRead = 0
|
|
@@ -568,7 +589,6 @@ class Parser {
|
|
|
568
589
|
this.keepAlive = ''
|
|
569
590
|
this.connection = ''
|
|
570
591
|
|
|
571
|
-
assert(this.headers.length % 2 === 0)
|
|
572
592
|
this.headers = []
|
|
573
593
|
this.headersSize = 0
|
|
574
594
|
|
|
@@ -587,7 +607,7 @@ class Parser {
|
|
|
587
607
|
client[kQueue][client[kRunningIdx]++] = null
|
|
588
608
|
|
|
589
609
|
if (socket[kWriting]) {
|
|
590
|
-
assert
|
|
610
|
+
assert(client[kRunning] === 0)
|
|
591
611
|
// Response completed before request.
|
|
592
612
|
util.destroy(socket, new InformationalError('reset'))
|
|
593
613
|
return constants.ERROR.PAUSED
|
|
@@ -613,19 +633,19 @@ class Parser {
|
|
|
613
633
|
}
|
|
614
634
|
|
|
615
635
|
function onParserTimeout (parser) {
|
|
616
|
-
const { socket, timeoutType, client } = parser
|
|
636
|
+
const { socket, timeoutType, client, paused } = parser.deref()
|
|
617
637
|
|
|
618
638
|
/* istanbul ignore else */
|
|
619
639
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
620
640
|
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
|
621
|
-
assert(!
|
|
641
|
+
assert(!paused, 'cannot be paused while waiting for headers')
|
|
622
642
|
util.destroy(socket, new HeadersTimeoutError())
|
|
623
643
|
}
|
|
624
644
|
} else if (timeoutType === TIMEOUT_BODY) {
|
|
625
|
-
if (!
|
|
645
|
+
if (!paused) {
|
|
626
646
|
util.destroy(socket, new BodyTimeoutError())
|
|
627
647
|
}
|
|
628
|
-
} else if (timeoutType ===
|
|
648
|
+
} else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
|
|
629
649
|
assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
|
|
630
650
|
util.destroy(socket, new InformationalError('socket idle timeout'))
|
|
631
651
|
}
|
|
@@ -646,10 +666,10 @@ async function connectH1 (client, socket) {
|
|
|
646
666
|
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
|
647
667
|
|
|
648
668
|
addListener(socket, 'error', function (err) {
|
|
649
|
-
const parser = this[kParser]
|
|
650
|
-
|
|
651
669
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
652
670
|
|
|
671
|
+
const parser = this[kParser]
|
|
672
|
+
|
|
653
673
|
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
654
674
|
// to the user.
|
|
655
675
|
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
|
|
@@ -803,8 +823,8 @@ function resumeH1 (client) {
|
|
|
803
823
|
}
|
|
804
824
|
|
|
805
825
|
if (client[kSize] === 0) {
|
|
806
|
-
if (socket[kParser].timeoutType !==
|
|
807
|
-
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue],
|
|
826
|
+
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
|
|
827
|
+
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
|
|
808
828
|
}
|
|
809
829
|
} else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
|
|
810
830
|
if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
|
package/lib/dispatcher/client.js
CHANGED