Skip to content

Commit 5d28d98

Browse files
nodejs-github-botmarco-ippolito
authored andcommittedNov 17, 2024
deps: update undici to 6.20.0
PR-URL: #55329 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]> Reviewed-By: Matthew Aitken <[email protected]> Reviewed-By: Rafael Gonzaga <[email protected]>
1 parent 0f81eaf commit 5d28d98

27 files changed

+1528
-608
lines changed
 

‎deps/undici/src/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ undici-fetch.js
8484
.npmrc
8585

8686
.tap
87+
88+
# File generated by /test/request-timeout.js
89+
test/request-timeout.10mb.bin

‎deps/undici/src/.npmignore

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ lib/llhttp/llhttp.wasm
1111
!index.d.ts
1212
!docs/docs/**/*
1313
!scripts/strip-comments.js
14+
15+
# File generated by /test/request-timeout.js
16+
test/request-timeout.10mb.bin

‎deps/undici/src/docs/docs/api/Dispatcher.md

+197
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,203 @@ client.dispatch(
984984
);
985985
```
986986

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+
9871184
## Instance Events
9881185
9891186
### Event: `'connect'`

‎deps/undici/src/docs/docs/api/RetryHandler.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
1919

2020
#### `RetryOptions`
2121

22-
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
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.
2323
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
2424
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
2525
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)

‎deps/undici/src/lib/api/api-upgrade.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ class UpgradeHandler extends AsyncResource {
5050
}
5151

5252
onUpgrade (statusCode, rawHeaders, socket) {
53-
const { callback, opaque, context } = this
53+
assert(statusCode === 101)
5454

55-
assert.strictEqual(statusCode, 101)
55+
const { callback, opaque, context } = this
5656

5757
removeSignal(this)
5858

‎deps/undici/src/lib/core/connect.js

+71-29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const net = require('node:net')
44
const assert = require('node:assert')
55
const util = require('./util')
66
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
7+
const timers = require('../util/timers')
8+
9+
function noop () {}
710

811
let tls // include tls conditionally since it is not always available
912

@@ -91,9 +94,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
9194
servername = servername || options.servername || util.getServerName(host) || null
9295

9396
const sessionKey = servername || hostname
97+
assert(sessionKey)
98+
9499
const session = customSession || sessionCache.get(sessionKey) || null
95100

96-
assert(sessionKey)
101+
port = port || 443
97102

98103
socket = tls.connect({
99104
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
@@ -104,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
104109
// TODO(HTTP/2): Add support for h2c
105110
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
106111
socket: httpSocket, // upgrade socket connection
107-
port: port || 443,
112+
port,
108113
host: hostname
109114
})
110115

@@ -115,11 +120,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
115120
})
116121
} else {
117122
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
123+
124+
port = port || 80
125+
118126
socket = net.connect({
119127
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
120128
...options,
121129
localAddress,
122-
port: port || 80,
130+
port,
123131
host: hostname
124132
})
125133
}
@@ -130,12 +138,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
130138
socket.setKeepAlive(true, keepAliveInitialDelay)
131139
}
132140

133-
const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
141+
const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
134142

135143
socket
136144
.setNoDelay(true)
137145
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
138-
cancelTimeout()
146+
queueMicrotask(clearConnectTimeout)
139147

140148
if (callback) {
141149
const cb = callback
@@ -144,7 +152,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
144152
}
145153
})
146154
.on('error', function (err) {
147-
cancelTimeout()
155+
queueMicrotask(clearConnectTimeout)
148156

149157
if (callback) {
150158
const cb = callback
@@ -157,36 +165,70 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
157165
}
158166
}
159167

160-
function setupTimeout (onConnectTimeout, timeout) {
161-
if (!timeout) {
162-
return () => {}
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+
}
164181

165-
let s1 = null
166-
let s2 = null
167-
const timeoutId = setTimeout(() => {
168-
// setImmediate is added to make sure that we prioritize socket error events over timeouts
169-
s1 = setImmediate(() => {
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(() => {
171187
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
172-
s2 = setImmediate(() => onConnectTimeout())
173-
} else {
174-
onConnectTimeout()
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
175200
}
176-
})
177-
}, timeout)
178-
return () => {
179-
clearTimeout(timeoutId)
180-
clearImmediate(s1)
181-
clearImmediate(s2)
182-
}
183-
}
184201

185-
function onConnectTimeout (socket) {
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) {
186223
let message = 'Connect Timeout Error'
187224
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},`
189228
}
229+
230+
message += ` timeout: ${opts.timeout}ms)`
231+
190232
util.destroy(socket, new ConnectTimeoutError(message))
191233
}
192234

‎deps/undici/src/lib/core/errors.js

+13
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,18 @@ class RequestRetryError extends UndiciError {
195195
}
196196
}
197197

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+
198210
class SecureProxyConnectionError extends UndiciError {
199211
constructor (cause, message, options) {
200212
super(message, { cause, ...(options ?? {}) })
@@ -227,5 +239,6 @@ module.exports = {
227239
BalancedPoolMissingUpstreamError,
228240
ResponseExceededMaxSizeError,
229241
RequestRetryError,
242+
ResponseError,
230243
SecureProxyConnectionError
231244
}

‎deps/undici/src/lib/core/util.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function getServerName (host) {
233233
return null
234234
}
235235

236-
assert.strictEqual(typeof host, 'string')
236+
assert(typeof host === 'string')
237237

238238
const servername = getHostname(host)
239239
if (net.isIP(servername)) {

‎deps/undici/src/lib/dispatcher/client-h1.js

+62-42
Original file line numberDiff line numberDiff line change
@@ -85,35 +85,35 @@ async function lazyllhttp () {
8585
return 0
8686
},
8787
wasm_on_status: (p, at, len) => {
88-
assert.strictEqual(currentParser.ptr, p)
88+
assert(currentParser.ptr === p)
8989
const start = at - currentBufferPtr + currentBufferRef.byteOffset
9090
return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
9191
},
9292
wasm_on_message_begin: (p) => {
93-
assert.strictEqual(currentParser.ptr, p)
93+
assert(currentParser.ptr === p)
9494
return currentParser.onMessageBegin() || 0
9595
},
9696
wasm_on_header_field: (p, at, len) => {
97-
assert.strictEqual(currentParser.ptr, p)
97+
assert(currentParser.ptr === p)
9898
const start = at - currentBufferPtr + currentBufferRef.byteOffset
9999
return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
100100
},
101101
wasm_on_header_value: (p, at, len) => {
102-
assert.strictEqual(currentParser.ptr, p)
102+
assert(currentParser.ptr === p)
103103
const start = at - currentBufferPtr + currentBufferRef.byteOffset
104104
return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
105105
},
106106
wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
107-
assert.strictEqual(currentParser.ptr, p)
107+
assert(currentParser.ptr === p)
108108
return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0
109109
},
110110
wasm_on_body: (p, at, len) => {
111-
assert.strictEqual(currentParser.ptr, p)
111+
assert(currentParser.ptr === p)
112112
const start = at - currentBufferPtr + currentBufferRef.byteOffset
113113
return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
114114
},
115115
wasm_on_message_complete: (p) => {
116-
assert.strictEqual(currentParser.ptr, p)
116+
assert(currentParser.ptr === p)
117117
return currentParser.onMessageComplete() || 0
118118
}
119119

@@ -131,9 +131,17 @@ let currentBufferRef = null
131131
let currentBufferSize = 0
132132
let currentBufferPtr = null
133133

134-
const TIMEOUT_HEADERS = 1
135-
const TIMEOUT_BODY = 2
136-
const TIMEOUT_IDLE = 3
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
137145

138146
class Parser {
139147
constructor (client, socket, { exports }) {
@@ -164,26 +172,39 @@ class Parser {
164172
this.maxResponseSize = client[kMaxResponseSize]
165173
}
166174

167-
setTimeout (value, type) {
168-
this.timeoutType = type
169-
if (value !== this.timeoutValue) {
170-
timers.clearTimeout(this.timeout)
171-
if (value) {
172-
this.timeout = timers.setTimeout(onParserTimeout, value, this)
173-
// istanbul ignore else: only for jest
174-
if (this.timeout.unref) {
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))
175195
this.timeout.unref()
176196
}
177-
} else {
178-
this.timeout = null
179197
}
180-
this.timeoutValue = value
198+
199+
this.timeoutValue = delay
181200
} else if (this.timeout) {
182201
// istanbul ignore else: only for jest
183202
if (this.timeout.refresh) {
184203
this.timeout.refresh()
185204
}
186205
}
206+
207+
this.timeoutType = type
187208
}
188209

189210
resume () {
@@ -288,7 +309,7 @@ class Parser {
288309
this.llhttp.llhttp_free(this.ptr)
289310
this.ptr = null
290311

291-
timers.clearTimeout(this.timeout)
312+
this.timeout && timers.clearTimeout(this.timeout)
292313
this.timeout = null
293314
this.timeoutValue = null
294315
this.timeoutType = null
@@ -363,20 +384,19 @@ class Parser {
363384
const { upgrade, client, socket, headers, statusCode } = this
364385

365386
assert(upgrade)
387+
assert(client[kSocket] === socket)
388+
assert(!socket.destroyed)
389+
assert(!this.paused)
390+
assert((headers.length & 1) === 0)
366391

367392
const request = client[kQueue][client[kRunningIdx]]
368393
assert(request)
369-
370-
assert(!socket.destroyed)
371-
assert(socket === client[kSocket])
372-
assert(!this.paused)
373394
assert(request.upgrade || request.method === 'CONNECT')
374395

375396
this.statusCode = null
376397
this.statusText = ''
377398
this.shouldKeepAlive = null
378399

379-
assert(this.headers.length % 2 === 0)
380400
this.headers = []
381401
this.headersSize = 0
382402

@@ -433,7 +453,7 @@ class Parser {
433453
return -1
434454
}
435455

436-
assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS)
456+
assert(this.timeoutType === TIMEOUT_HEADERS)
437457

438458
this.statusCode = statusCode
439459
this.shouldKeepAlive = (
@@ -466,7 +486,7 @@ class Parser {
466486
return 2
467487
}
468488

469-
assert(this.headers.length % 2 === 0)
489+
assert((this.headers.length & 1) === 0)
470490
this.headers = []
471491
this.headersSize = 0
472492

@@ -523,7 +543,7 @@ class Parser {
523543
const request = client[kQueue][client[kRunningIdx]]
524544
assert(request)
525545

526-
assert.strictEqual(this.timeoutType, TIMEOUT_BODY)
546+
assert(this.timeoutType === TIMEOUT_BODY)
527547
if (this.timeout) {
528548
// istanbul ignore else: only for jest
529549
if (this.timeout.refresh) {
@@ -556,19 +576,19 @@ class Parser {
556576
return
557577
}
558578

579+
assert(statusCode >= 100)
580+
assert((this.headers.length & 1) === 0)
581+
559582
const request = client[kQueue][client[kRunningIdx]]
560583
assert(request)
561584

562-
assert(statusCode >= 100)
563-
564585
this.statusCode = null
565586
this.statusText = ''
566587
this.bytesRead = 0
567588
this.contentLength = ''
568589
this.keepAlive = ''
569590
this.connection = ''
570591

571-
assert(this.headers.length % 2 === 0)
572592
this.headers = []
573593
this.headersSize = 0
574594

@@ -587,7 +607,7 @@ class Parser {
587607
client[kQueue][client[kRunningIdx]++] = null
588608

589609
if (socket[kWriting]) {
590-
assert.strictEqual(client[kRunning], 0)
610+
assert(client[kRunning] === 0)
591611
// Response completed before request.
592612
util.destroy(socket, new InformationalError('reset'))
593613
return constants.ERROR.PAUSED
@@ -613,19 +633,19 @@ class Parser {
613633
}
614634

615635
function onParserTimeout (parser) {
616-
const { socket, timeoutType, client } = parser
636+
const { socket, timeoutType, client, paused } = parser.deref()
617637

618638
/* istanbul ignore else */
619639
if (timeoutType === TIMEOUT_HEADERS) {
620640
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
621-
assert(!parser.paused, 'cannot be paused while waiting for headers')
641+
assert(!paused, 'cannot be paused while waiting for headers')
622642
util.destroy(socket, new HeadersTimeoutError())
623643
}
624644
} else if (timeoutType === TIMEOUT_BODY) {
625-
if (!parser.paused) {
645+
if (!paused) {
626646
util.destroy(socket, new BodyTimeoutError())
627647
}
628-
} else if (timeoutType === TIMEOUT_IDLE) {
648+
} else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
629649
assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
630650
util.destroy(socket, new InformationalError('socket idle timeout'))
631651
}
@@ -646,10 +666,10 @@ async function connectH1 (client, socket) {
646666
socket[kParser] = new Parser(client, socket, llhttpInstance)
647667

648668
addListener(socket, 'error', function (err) {
649-
const parser = this[kParser]
650-
651669
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
652670

671+
const parser = this[kParser]
672+
653673
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
654674
// to the user.
655675
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
@@ -803,8 +823,8 @@ function resumeH1 (client) {
803823
}
804824

805825
if (client[kSize] === 0) {
806-
if (socket[kParser].timeoutType !== TIMEOUT_IDLE) {
807-
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE)
826+
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
827+
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
808828
}
809829
} else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
810830
if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {

‎deps/undici/src/lib/dispatcher/client.js

+4
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ function onError (client, err) {
385385
}
386386
}
387387

388+
/**
389+
* @param {Client} client
390+
* @returns
391+
*/
388392
async function connect (client) {
389393
assert(!client[kConnecting])
390394
assert(!client[kHTTPContext])

‎deps/undici/src/lib/handler/retry-handler.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,18 @@ class RetryHandler {
192192
if (this.resume != null) {
193193
this.resume = null
194194

195-
if (statusCode !== 206) {
196-
return true
195+
// Only Partial Content 206 supposed to provide Content-Range,
196+
// any other status code that partially consumed the payload
197+
// should not be retry because it would result in downstream
198+
// wrongly concatanete multiple responses.
199+
if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
200+
this.abort(
201+
new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
202+
headers,
203+
data: { count: this.retryCount }
204+
})
205+
)
206+
return false
197207
}
198208

199209
const contentRange = parseRangeHeader(headers['content-range'])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict'
2+
3+
const { parseHeaders } = require('../core/util')
4+
const DecoratorHandler = require('../handler/decorator-handler')
5+
const { ResponseError } = require('../core/errors')
6+
7+
class Handler extends DecoratorHandler {
8+
#handler
9+
#statusCode
10+
#contentType
11+
#decoder
12+
#headers
13+
#body
14+
15+
constructor (opts, { handler }) {
16+
super(handler)
17+
this.#handler = handler
18+
}
19+
20+
onConnect (abort) {
21+
this.#statusCode = 0
22+
this.#contentType = null
23+
this.#decoder = null
24+
this.#headers = null
25+
this.#body = ''
26+
27+
return this.#handler.onConnect(abort)
28+
}
29+
30+
onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
31+
this.#statusCode = statusCode
32+
this.#headers = headers
33+
this.#contentType = headers['content-type']
34+
35+
if (this.#statusCode < 400) {
36+
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
37+
}
38+
39+
if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
40+
this.#decoder = new TextDecoder('utf-8')
41+
}
42+
}
43+
44+
onData (chunk) {
45+
if (this.#statusCode < 400) {
46+
return this.#handler.onData(chunk)
47+
}
48+
49+
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
50+
}
51+
52+
onComplete (rawTrailers) {
53+
if (this.#statusCode >= 400) {
54+
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
55+
56+
if (this.#contentType === 'application/json') {
57+
try {
58+
this.#body = JSON.parse(this.#body)
59+
} catch {
60+
// Do nothing...
61+
}
62+
}
63+
64+
let err
65+
const stackTraceLimit = Error.stackTraceLimit
66+
Error.stackTraceLimit = 0
67+
try {
68+
err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body)
69+
} finally {
70+
Error.stackTraceLimit = stackTraceLimit
71+
}
72+
73+
this.#handler.onError(err)
74+
} else {
75+
this.#handler.onComplete(rawTrailers)
76+
}
77+
}
78+
79+
onError (err) {
80+
this.#handler.onError(err)
81+
}
82+
}
83+
84+
module.exports = (dispatch) => (opts, handler) => opts.throwOnError
85+
? dispatch(opts, new Handler(opts, { handler }))
86+
: dispatch(opts, handler)

‎deps/undici/src/lib/llhttp/wasm_build_env.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11

2-
> undici@6.19.8 prebuild:wasm
2+
> undici@6.20.0 prebuild:wasm
33
> node build/wasm.js --prebuild
44

55
> docker build --platform=linux/x86_64 -t llhttp_wasm_builder -f /home/runner/work/node/node/deps/undici/src/build/Dockerfile /home/runner/work/node/node/deps/undici/src
66

77

88

9-
> undici@6.19.8 build:wasm
9+
> undici@6.20.0 build:wasm
1010
> node build/wasm.js --docker
1111

1212
> docker run --rm -t --platform=linux/x86_64 --user 1001:127 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js

‎deps/undici/src/lib/mock/mock-utils.js

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ function matchKey (mockDispatch, { path, method, body, headers }) {
118118
function getResponseData (data) {
119119
if (Buffer.isBuffer(data)) {
120120
return data
121+
} else if (data instanceof Uint8Array) {
122+
return data
123+
} else if (data instanceof ArrayBuffer) {
124+
return data
121125
} else if (typeof data === 'object') {
122126
return JSON.stringify(data)
123127
} else {

‎deps/undici/src/lib/util/timers.js

+367-43
Large diffs are not rendered by default.

‎deps/undici/src/lib/web/fetch/formdata-parser.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,21 @@ function multipartFormDataParser (input, mimeType) {
8787
// the first byte.
8888
const position = { position: 0 }
8989

90-
// Note: undici addition, allow \r\n before the body.
91-
if (input[0] === 0x0d && input[1] === 0x0a) {
90+
// Note: undici addition, allows leading and trailing CRLFs.
91+
while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
9292
position.position += 2
9393
}
9494

95+
let trailing = input.length
96+
97+
while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
98+
trailing -= 2
99+
}
100+
101+
if (trailing !== input.length) {
102+
input = input.subarray(0, trailing)
103+
}
104+
95105
// 5. While true:
96106
while (true) {
97107
// 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D

‎deps/undici/src/lib/web/fetch/index.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -2150,23 +2150,35 @@ async function httpNetworkFetch (
21502150
finishFlush: zlib.constants.Z_SYNC_FLUSH
21512151
}))
21522152
} else if (coding === 'deflate') {
2153-
decoders.push(createInflate())
2153+
decoders.push(createInflate({
2154+
flush: zlib.constants.Z_SYNC_FLUSH,
2155+
finishFlush: zlib.constants.Z_SYNC_FLUSH
2156+
}))
21542157
} else if (coding === 'br') {
2155-
decoders.push(zlib.createBrotliDecompress())
2158+
decoders.push(zlib.createBrotliDecompress({
2159+
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
2160+
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
2161+
}))
21562162
} else {
21572163
decoders.length = 0
21582164
break
21592165
}
21602166
}
21612167
}
21622168

2169+
const onError = this.onError.bind(this)
2170+
21632171
resolve({
21642172
status,
21652173
statusText,
21662174
headersList,
21672175
body: decoders.length
2168-
? pipeline(this.body, ...decoders, () => { })
2169-
: this.body.on('error', () => { })
2176+
? pipeline(this.body, ...decoders, (err) => {
2177+
if (err) {
2178+
this.onError(err)
2179+
}
2180+
}).on('error', onError)
2181+
: this.body.on('error', onError)
21702182
})
21712183

21722184
return true

‎deps/undici/src/lib/web/fetch/util.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -1339,15 +1339,23 @@ function buildContentRange (rangeStart, rangeEnd, fullLength) {
13391339
// interpreted as a zlib stream, otherwise it's interpreted as a
13401340
// raw deflate stream.
13411341
class InflateStream extends Transform {
1342+
#zlibOptions
1343+
1344+
/** @param {zlib.ZlibOptions} [zlibOptions] */
1345+
constructor (zlibOptions) {
1346+
super()
1347+
this.#zlibOptions = zlibOptions
1348+
}
1349+
13421350
_transform (chunk, encoding, callback) {
13431351
if (!this._inflateStream) {
13441352
if (chunk.length === 0) {
13451353
callback()
13461354
return
13471355
}
13481356
this._inflateStream = (chunk[0] & 0x0F) === 0x08
1349-
? zlib.createInflate()
1350-
: zlib.createInflateRaw()
1357+
? zlib.createInflate(this.#zlibOptions)
1358+
: zlib.createInflateRaw(this.#zlibOptions)
13511359

13521360
this._inflateStream.on('data', this.push.bind(this))
13531361
this._inflateStream.on('end', () => this.push(null))
@@ -1366,8 +1374,12 @@ class InflateStream extends Transform {
13661374
}
13671375
}
13681376

1369-
function createInflate () {
1370-
return new InflateStream()
1377+
/**
1378+
* @param {zlib.ZlibOptions} [zlibOptions]
1379+
* @returns {InflateStream}
1380+
*/
1381+
function createInflate (zlibOptions) {
1382+
return new InflateStream(zlibOptions)
13711383
}
13721384

13731385
/**

‎deps/undici/src/package-lock.json

+284-271
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deps/undici/src/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "undici",
3-
"version": "6.19.8",
3+
"version": "6.20.0",
44
"description": "An HTTP/1.1 client, written from scratch for Node.js",
55
"homepage": "https://undici.nodejs.org",
66
"bugs": {
@@ -105,7 +105,7 @@
105105
"@fastify/busboy": "2.1.1",
106106
"@matteo.collina/tspl": "^0.1.1",
107107
"@sinonjs/fake-timers": "^11.1.0",
108-
"@types/node": "^18.0.3",
108+
"@types/node": "~18.19.50",
109109
"abort-controller": "^3.0.0",
110110
"borp": "^0.15.0",
111111
"c8": "^10.0.0",

‎deps/undici/src/types/eventsource.d.ts

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { MessageEvent, ErrorEvent } from './websocket'
22
import Dispatcher from './dispatcher'
33

44
import {
5-
EventTarget,
6-
Event,
75
EventListenerOptions,
86
AddEventListenerOptions,
97
EventListenerOrEventListenerObject

‎deps/undici/src/types/filereader.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference types="node" />
22

33
import { Blob } from 'buffer'
4-
import { DOMException, Event, EventInit, EventTarget } from './patch'
4+
import { DOMException, EventInit } from './patch'
55

66
export declare class FileReader {
77
__proto__: EventTarget & FileReader

‎deps/undici/src/types/interceptors.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ declare namespace Interceptors {
77
export type DumpInterceptorOpts = { maxSize?: number }
88
export type RetryInterceptorOpts = RetryHandler.RetryOptions
99
export type RedirectInterceptorOpts = { maxRedirections?: number }
10-
10+
export type ResponseErrorInterceptorOpts = { throwOnError: boolean }
11+
1112
export function createRedirectInterceptor(opts: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
1213
export function dump(opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
1314
export function retry(opts?: RetryInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
1415
export function redirect(opts?: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
16+
export function responseError(opts?: ResponseErrorInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
1517
}

‎deps/undici/src/types/patch.d.ts

-38
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,6 @@ export type DOMException = typeof globalThis extends { DOMException: infer T }
66
? T
77
: any
88

9-
export type EventTarget = typeof globalThis extends { EventTarget: infer T }
10-
? T
11-
: {
12-
addEventListener(
13-
type: string,
14-
listener: any,
15-
options?: any,
16-
): void
17-
dispatchEvent(event: Event): boolean
18-
removeEventListener(
19-
type: string,
20-
listener: any,
21-
options?: any | boolean,
22-
): void
23-
}
24-
25-
export type Event = typeof globalThis extends { Event: infer T }
26-
? T
27-
: {
28-
readonly bubbles: boolean
29-
cancelBubble: () => void
30-
readonly cancelable: boolean
31-
readonly composed: boolean
32-
composedPath(): [EventTarget?]
33-
readonly currentTarget: EventTarget | null
34-
readonly defaultPrevented: boolean
35-
readonly eventPhase: 0 | 2
36-
readonly isTrusted: boolean
37-
preventDefault(): void
38-
returnValue: boolean
39-
readonly srcElement: EventTarget | null
40-
stopImmediatePropagation(): void
41-
stopPropagation(): void
42-
readonly target: EventTarget | null
43-
readonly timeStamp: number
44-
readonly type: string
45-
}
46-
479
export interface EventInit {
4810
bubbles?: boolean
4911
cancelable?: boolean

‎deps/undici/src/types/websocket.d.ts

-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import type { Blob } from 'buffer'
44
import type { MessagePort } from 'worker_threads'
55
import {
6-
EventTarget,
7-
Event,
86
EventInit,
97
EventListenerOptions,
108
AddEventListenerOptions,

‎deps/undici/undici.js

+365-158
Large diffs are not rendered by default.

‎src/undici_version.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
// Refer to tools/dep_updaters/update-undici.sh
33
#ifndef SRC_UNDICI_VERSION_H_
44
#define SRC_UNDICI_VERSION_H_
5-
#define UNDICI_VERSION "6.19.8"
5+
#define UNDICI_VERSION "6.20.0"
66
#endif // SRC_UNDICI_VERSION_H_

0 commit comments

Comments
 (0)
Please sign in to comment.