undici 6.6.1 → 6.7.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/{README.md → docs/README.md} +19 -15
- package/docs/{api → docs/api}/Dispatcher.md +39 -3
- package/docs/docs/api/Fetch.md +57 -0
- package/docs/{api → docs/api}/ProxyAgent.md +3 -1
- package/docs/docs/api/RetryAgent.md +45 -0
- package/docs/{api → docs/api}/RetryHandler.md +1 -1
- package/docs/{api → docs/api}/api-lifecycle.md +33 -4
- package/docs/{best-practices → docs/best-practices}/proxy.md +6 -6
- package/index-fetch.js +11 -7
- package/index.js +31 -25
- package/lib/core/request.js +72 -135
- package/lib/core/symbols.js +6 -5
- package/lib/core/tree.js +46 -26
- package/lib/core/util.js +41 -20
- package/lib/{agent.js → dispatcher/agent.js} +4 -4
- package/lib/{balanced-pool.js → dispatcher/balanced-pool.js} +3 -3
- package/lib/dispatcher/client-h1.js +1339 -0
- package/lib/dispatcher/client-h2.js +639 -0
- package/lib/dispatcher/client.js +611 -0
- package/lib/{dispatcher-base.js → dispatcher/dispatcher-base.js} +2 -2
- package/lib/{pool-base.js → dispatcher/pool-base.js} +3 -3
- package/lib/{pool-stats.js → dispatcher/pool-stats.js} +1 -1
- package/lib/{pool.js → dispatcher/pool.js} +4 -4
- package/lib/{proxy-agent.js → dispatcher/proxy-agent.js} +29 -35
- package/lib/dispatcher/retry-agent.js +35 -0
- package/lib/global.js +1 -1
- package/lib/handler/{RetryHandler.js → retry-handler.js} +2 -2
- package/lib/interceptor/{redirectInterceptor.js → redirect-interceptor.js} +1 -1
- package/lib/mock/mock-agent.js +2 -2
- package/lib/mock/mock-client.js +1 -1
- package/lib/mock/mock-interceptor.js +2 -2
- package/lib/mock/mock-pool.js +1 -1
- package/lib/mock/mock-utils.js +6 -4
- package/lib/{cache → web/cache}/cache.js +2 -4
- package/lib/{cache → web/cache}/cachestorage.js +1 -1
- package/lib/web/cache/symbols.js +5 -0
- package/lib/{cache → web/cache}/util.js +5 -9
- package/lib/{cookies → web/cookies}/parse.js +1 -1
- package/lib/{cookies → web/cookies}/util.js +76 -60
- package/lib/{eventsource → web/eventsource}/eventsource.js +2 -6
- package/lib/{fetch → web/fetch}/body.js +23 -52
- package/lib/{fetch/dataURL.js → web/fetch/data-url.js} +2 -0
- package/lib/{compat → web/fetch}/dispatcher-weakref.js +1 -1
- package/lib/{fetch → web/fetch}/file.js +2 -2
- package/lib/{fetch → web/fetch}/formdata.js +6 -67
- package/lib/{fetch → web/fetch}/headers.js +99 -71
- package/lib/{fetch → web/fetch}/index.js +40 -31
- package/lib/{fetch → web/fetch}/request.js +14 -6
- package/lib/{fetch → web/fetch}/response.js +3 -3
- package/lib/{fetch → web/fetch}/symbols.js +2 -1
- package/lib/{fetch → web/fetch}/util.js +142 -48
- package/lib/{fetch → web/fetch}/webidl.js +53 -19
- package/lib/{fileapi → web/fileapi}/filereader.js +1 -1
- package/lib/{fileapi → web/fileapi}/util.js +1 -1
- package/lib/{websocket → web/websocket}/connection.js +20 -10
- package/lib/{websocket → web/websocket}/constants.js +7 -0
- package/lib/{websocket → web/websocket}/events.js +1 -1
- package/lib/{websocket → web/websocket}/frame.js +1 -0
- package/lib/{websocket → web/websocket}/receiver.js +9 -16
- package/lib/{websocket → web/websocket}/util.js +37 -23
- package/lib/{websocket → web/websocket}/websocket.js +21 -9
- package/package.json +27 -52
- package/types/dispatcher.d.ts +1 -1
- package/types/fetch.d.ts +20 -21
- package/types/index.d.ts +2 -1
- package/types/retry-agent.d.ts +11 -0
- package/types/webidl.d.ts +6 -1
- package/docs/api/Fetch.md +0 -27
- package/docs/assets/lifecycle-diagram.png +0 -0
- package/lib/cache/symbols.js +0 -5
- package/lib/client.js +0 -2295
- package/lib/llhttp/llhttp-wasm.js +0 -3
- package/lib/llhttp/llhttp.wasm +0 -0
- package/lib/llhttp/llhttp_simd.wasm +0 -0
- /package/docs/{api → docs/api}/Agent.md +0 -0
- /package/docs/{api → docs/api}/BalancedPool.md +0 -0
- /package/docs/{api → docs/api}/CacheStorage.md +0 -0
- /package/docs/{api → docs/api}/Client.md +0 -0
- /package/docs/{api → docs/api}/Connector.md +0 -0
- /package/docs/{api → docs/api}/ContentType.md +0 -0
- /package/docs/{api → docs/api}/Cookies.md +0 -0
- /package/docs/{api → docs/api}/Debug.md +0 -0
- /package/docs/{api → docs/api}/DiagnosticsChannel.md +0 -0
- /package/docs/{api → docs/api}/DispatchInterceptor.md +0 -0
- /package/docs/{api → docs/api}/Errors.md +0 -0
- /package/docs/{api → docs/api}/EventSource.md +0 -0
- /package/docs/{api → docs/api}/MockAgent.md +0 -0
- /package/docs/{api → docs/api}/MockClient.md +0 -0
- /package/docs/{api → docs/api}/MockErrors.md +0 -0
- /package/docs/{api → docs/api}/MockPool.md +0 -0
- /package/docs/{api → docs/api}/Pool.md +0 -0
- /package/docs/{api → docs/api}/PoolStats.md +0 -0
- /package/docs/{api → docs/api}/RedirectHandler.md +0 -0
- /package/docs/{api → docs/api}/Util.md +0 -0
- /package/docs/{api → docs/api}/WebSocket.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/client-certificate.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/mocking-request.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/writing-tests.md +0 -0
- /package/lib/{dispatcher.js → dispatcher/dispatcher.js} +0 -0
- /package/lib/{node → dispatcher}/fixed-queue.js +0 -0
- /package/lib/handler/{DecoratorHandler.js → decorator-handler.js} +0 -0
- /package/lib/handler/{RedirectHandler.js → redirect-handler.js} +0 -0
- /package/lib/{timers.js → util/timers.js} +0 -0
- /package/lib/{cookies → web/cookies}/constants.js +0 -0
- /package/lib/{cookies → web/cookies}/index.js +0 -0
- /package/lib/{eventsource → web/eventsource}/eventsource-stream.js +0 -0
- /package/lib/{eventsource → web/eventsource}/util.js +0 -0
- /package/lib/{fetch → web/fetch}/LICENSE +0 -0
- /package/lib/{fetch → web/fetch}/constants.js +0 -0
- /package/lib/{fetch → web/fetch}/global.js +0 -0
- /package/lib/{fileapi → web/fileapi}/encoding.js +0 -0
- /package/lib/{fileapi → web/fileapi}/progressevent.js +0 -0
- /package/lib/{fileapi → web/fileapi}/symbols.js +0 -0
- /package/lib/{websocket → web/websocket}/symbols.js +0 -0
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* global WebAssembly */
|
|
4
|
+
|
|
5
|
+
const assert = require('node:assert')
|
|
6
|
+
const util = require('../core/util.js')
|
|
7
|
+
const { channels } = require('../core/diagnostics.js')
|
|
8
|
+
const timers = require('../util/timers.js')
|
|
9
|
+
const {
|
|
10
|
+
RequestContentLengthMismatchError,
|
|
11
|
+
ResponseContentLengthMismatchError,
|
|
12
|
+
RequestAbortedError,
|
|
13
|
+
HeadersTimeoutError,
|
|
14
|
+
HeadersOverflowError,
|
|
15
|
+
SocketError,
|
|
16
|
+
InformationalError,
|
|
17
|
+
BodyTimeoutError,
|
|
18
|
+
HTTPParserError,
|
|
19
|
+
ResponseExceededMaxSizeError
|
|
20
|
+
} = require('../core/errors.js')
|
|
21
|
+
const {
|
|
22
|
+
kUrl,
|
|
23
|
+
kReset,
|
|
24
|
+
kClient,
|
|
25
|
+
kParser,
|
|
26
|
+
kBlocking,
|
|
27
|
+
kRunning,
|
|
28
|
+
kPending,
|
|
29
|
+
kSize,
|
|
30
|
+
kWriting,
|
|
31
|
+
kQueue,
|
|
32
|
+
kNoRef,
|
|
33
|
+
kKeepAliveDefaultTimeout,
|
|
34
|
+
kHostHeader,
|
|
35
|
+
kPendingIdx,
|
|
36
|
+
kRunningIdx,
|
|
37
|
+
kError,
|
|
38
|
+
kPipelining,
|
|
39
|
+
kSocket,
|
|
40
|
+
kKeepAliveTimeoutValue,
|
|
41
|
+
kMaxHeadersSize,
|
|
42
|
+
kKeepAliveMaxTimeout,
|
|
43
|
+
kKeepAliveTimeoutThreshold,
|
|
44
|
+
kHeadersTimeout,
|
|
45
|
+
kBodyTimeout,
|
|
46
|
+
kStrictContentLength,
|
|
47
|
+
kMaxRequests,
|
|
48
|
+
kCounter,
|
|
49
|
+
kMaxResponseSize,
|
|
50
|
+
kListeners,
|
|
51
|
+
kOnError,
|
|
52
|
+
kResume,
|
|
53
|
+
kHTTPContext
|
|
54
|
+
} = require('../core/symbols.js')
|
|
55
|
+
|
|
56
|
+
const constants = require('../llhttp/constants.js')
|
|
57
|
+
const EMPTY_BUF = Buffer.alloc(0)
|
|
58
|
+
const FastBuffer = Buffer[Symbol.species]
|
|
59
|
+
|
|
60
|
+
let extractBody
|
|
61
|
+
|
|
62
|
+
function addListener (obj, name, listener) {
|
|
63
|
+
const listeners = (obj[kListeners] ??= [])
|
|
64
|
+
listeners.push([name, listener])
|
|
65
|
+
obj.on(name, listener)
|
|
66
|
+
return obj
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function removeAllListeners (obj) {
|
|
70
|
+
for (const [name, listener] of obj[kListeners] ?? []) {
|
|
71
|
+
obj.removeListener(name, listener)
|
|
72
|
+
}
|
|
73
|
+
obj[kListeners] = null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function lazyllhttp () {
|
|
77
|
+
const mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js'))
|
|
78
|
+
|
|
79
|
+
return await WebAssembly.instantiate(mod, {
|
|
80
|
+
env: {
|
|
81
|
+
/* eslint-disable camelcase */
|
|
82
|
+
|
|
83
|
+
wasm_on_url: (p, at, len) => {
|
|
84
|
+
/* istanbul ignore next */
|
|
85
|
+
return 0
|
|
86
|
+
},
|
|
87
|
+
wasm_on_status: (p, at, len) => {
|
|
88
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
89
|
+
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
90
|
+
return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
91
|
+
},
|
|
92
|
+
wasm_on_message_begin: (p) => {
|
|
93
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
94
|
+
return currentParser.onMessageBegin() || 0
|
|
95
|
+
},
|
|
96
|
+
wasm_on_header_field: (p, at, len) => {
|
|
97
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
98
|
+
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
99
|
+
return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
100
|
+
},
|
|
101
|
+
wasm_on_header_value: (p, at, len) => {
|
|
102
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
103
|
+
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
104
|
+
return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
105
|
+
},
|
|
106
|
+
wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
|
|
107
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
108
|
+
return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0
|
|
109
|
+
},
|
|
110
|
+
wasm_on_body: (p, at, len) => {
|
|
111
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
112
|
+
const start = at - currentBufferPtr + currentBufferRef.byteOffset
|
|
113
|
+
return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
|
|
114
|
+
},
|
|
115
|
+
wasm_on_message_complete: (p) => {
|
|
116
|
+
assert.strictEqual(currentParser.ptr, p)
|
|
117
|
+
return currentParser.onMessageComplete() || 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* eslint-enable camelcase */
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let llhttpInstance = null
|
|
126
|
+
let llhttpPromise = lazyllhttp()
|
|
127
|
+
llhttpPromise.catch()
|
|
128
|
+
|
|
129
|
+
let currentParser = null
|
|
130
|
+
let currentBufferRef = null
|
|
131
|
+
let currentBufferSize = 0
|
|
132
|
+
let currentBufferPtr = null
|
|
133
|
+
|
|
134
|
+
const TIMEOUT_HEADERS = 1
|
|
135
|
+
const TIMEOUT_BODY = 2
|
|
136
|
+
const TIMEOUT_IDLE = 3
|
|
137
|
+
|
|
138
|
+
class Parser {
|
|
139
|
+
constructor (client, socket, { exports }) {
|
|
140
|
+
assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0)
|
|
141
|
+
|
|
142
|
+
this.llhttp = exports
|
|
143
|
+
this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE)
|
|
144
|
+
this.client = client
|
|
145
|
+
this.socket = socket
|
|
146
|
+
this.timeout = null
|
|
147
|
+
this.timeoutValue = null
|
|
148
|
+
this.timeoutType = null
|
|
149
|
+
this.statusCode = null
|
|
150
|
+
this.statusText = ''
|
|
151
|
+
this.upgrade = false
|
|
152
|
+
this.headers = []
|
|
153
|
+
this.headersSize = 0
|
|
154
|
+
this.headersMaxSize = client[kMaxHeadersSize]
|
|
155
|
+
this.shouldKeepAlive = false
|
|
156
|
+
this.paused = false
|
|
157
|
+
this.resume = this.resume.bind(this)
|
|
158
|
+
|
|
159
|
+
this.bytesRead = 0
|
|
160
|
+
|
|
161
|
+
this.keepAlive = ''
|
|
162
|
+
this.contentLength = ''
|
|
163
|
+
this.connection = ''
|
|
164
|
+
this.maxResponseSize = client[kMaxResponseSize]
|
|
165
|
+
}
|
|
166
|
+
|
|
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
|
+
this.timeout.unref()
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
this.timeout = null
|
|
179
|
+
}
|
|
180
|
+
this.timeoutValue = value
|
|
181
|
+
} else if (this.timeout) {
|
|
182
|
+
// istanbul ignore else: only for jest
|
|
183
|
+
if (this.timeout.refresh) {
|
|
184
|
+
this.timeout.refresh()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
resume () {
|
|
190
|
+
if (this.socket.destroyed || !this.paused) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
assert(this.ptr != null)
|
|
195
|
+
assert(currentParser == null)
|
|
196
|
+
|
|
197
|
+
this.llhttp.llhttp_resume(this.ptr)
|
|
198
|
+
|
|
199
|
+
assert(this.timeoutType === TIMEOUT_BODY)
|
|
200
|
+
if (this.timeout) {
|
|
201
|
+
// istanbul ignore else: only for jest
|
|
202
|
+
if (this.timeout.refresh) {
|
|
203
|
+
this.timeout.refresh()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.paused = false
|
|
208
|
+
this.execute(this.socket.read() || EMPTY_BUF) // Flush parser.
|
|
209
|
+
this.readMore()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
readMore () {
|
|
213
|
+
while (!this.paused && this.ptr) {
|
|
214
|
+
const chunk = this.socket.read()
|
|
215
|
+
if (chunk === null) {
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
this.execute(chunk)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
execute (data) {
|
|
223
|
+
assert(this.ptr != null)
|
|
224
|
+
assert(currentParser == null)
|
|
225
|
+
assert(!this.paused)
|
|
226
|
+
|
|
227
|
+
const { socket, llhttp } = this
|
|
228
|
+
|
|
229
|
+
if (data.length > currentBufferSize) {
|
|
230
|
+
if (currentBufferPtr) {
|
|
231
|
+
llhttp.free(currentBufferPtr)
|
|
232
|
+
}
|
|
233
|
+
currentBufferSize = Math.ceil(data.length / 4096) * 4096
|
|
234
|
+
currentBufferPtr = llhttp.malloc(currentBufferSize)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data)
|
|
238
|
+
|
|
239
|
+
// Call `execute` on the wasm parser.
|
|
240
|
+
// We pass the `llhttp_parser` pointer address, the pointer address of buffer view data,
|
|
241
|
+
// and finally the length of bytes to parse.
|
|
242
|
+
// The return value is an error code or `constants.ERROR.OK`.
|
|
243
|
+
try {
|
|
244
|
+
let ret
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
currentBufferRef = data
|
|
248
|
+
currentParser = this
|
|
249
|
+
ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length)
|
|
250
|
+
/* eslint-disable-next-line no-useless-catch */
|
|
251
|
+
} catch (err) {
|
|
252
|
+
/* istanbul ignore next: difficult to make a test case for */
|
|
253
|
+
throw err
|
|
254
|
+
} finally {
|
|
255
|
+
currentParser = null
|
|
256
|
+
currentBufferRef = null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr
|
|
260
|
+
|
|
261
|
+
if (ret === constants.ERROR.PAUSED_UPGRADE) {
|
|
262
|
+
this.onUpgrade(data.slice(offset))
|
|
263
|
+
} else if (ret === constants.ERROR.PAUSED) {
|
|
264
|
+
this.paused = true
|
|
265
|
+
socket.unshift(data.slice(offset))
|
|
266
|
+
} else if (ret !== constants.ERROR.OK) {
|
|
267
|
+
const ptr = llhttp.llhttp_get_error_reason(this.ptr)
|
|
268
|
+
let message = ''
|
|
269
|
+
/* istanbul ignore else: difficult to make a test case for */
|
|
270
|
+
if (ptr) {
|
|
271
|
+
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
|
|
272
|
+
message =
|
|
273
|
+
'Response does not match the HTTP/1.1 protocol (' +
|
|
274
|
+
Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
|
|
275
|
+
')'
|
|
276
|
+
}
|
|
277
|
+
throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset))
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
util.destroy(socket, err)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
destroy () {
|
|
285
|
+
assert(this.ptr != null)
|
|
286
|
+
assert(currentParser == null)
|
|
287
|
+
|
|
288
|
+
this.llhttp.llhttp_free(this.ptr)
|
|
289
|
+
this.ptr = null
|
|
290
|
+
|
|
291
|
+
timers.clearTimeout(this.timeout)
|
|
292
|
+
this.timeout = null
|
|
293
|
+
this.timeoutValue = null
|
|
294
|
+
this.timeoutType = null
|
|
295
|
+
|
|
296
|
+
this.paused = false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
onStatus (buf) {
|
|
300
|
+
this.statusText = buf.toString()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
onMessageBegin () {
|
|
304
|
+
const { socket, client } = this
|
|
305
|
+
|
|
306
|
+
/* istanbul ignore next: difficult to make a test case for */
|
|
307
|
+
if (socket.destroyed) {
|
|
308
|
+
return -1
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
312
|
+
if (!request) {
|
|
313
|
+
return -1
|
|
314
|
+
}
|
|
315
|
+
request.onResponseStarted()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
onHeaderField (buf) {
|
|
319
|
+
const len = this.headers.length
|
|
320
|
+
|
|
321
|
+
if ((len & 1) === 0) {
|
|
322
|
+
this.headers.push(buf)
|
|
323
|
+
} else {
|
|
324
|
+
this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.trackHeader(buf.length)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
onHeaderValue (buf) {
|
|
331
|
+
let len = this.headers.length
|
|
332
|
+
|
|
333
|
+
if ((len & 1) === 1) {
|
|
334
|
+
this.headers.push(buf)
|
|
335
|
+
len += 1
|
|
336
|
+
} else {
|
|
337
|
+
this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const key = this.headers[len - 2]
|
|
341
|
+
if (key.length === 10) {
|
|
342
|
+
const headerName = util.bufferToLowerCasedHeaderName(key)
|
|
343
|
+
if (headerName === 'keep-alive') {
|
|
344
|
+
this.keepAlive += buf.toString()
|
|
345
|
+
} else if (headerName === 'connection') {
|
|
346
|
+
this.connection += buf.toString()
|
|
347
|
+
}
|
|
348
|
+
} else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') {
|
|
349
|
+
this.contentLength += buf.toString()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.trackHeader(buf.length)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
trackHeader (len) {
|
|
356
|
+
this.headersSize += len
|
|
357
|
+
if (this.headersSize >= this.headersMaxSize) {
|
|
358
|
+
util.destroy(this.socket, new HeadersOverflowError())
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
onUpgrade (head) {
|
|
363
|
+
const { upgrade, client, socket, headers, statusCode } = this
|
|
364
|
+
|
|
365
|
+
assert(upgrade)
|
|
366
|
+
|
|
367
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
368
|
+
assert(request)
|
|
369
|
+
|
|
370
|
+
assert(!socket.destroyed)
|
|
371
|
+
assert(socket === client[kSocket])
|
|
372
|
+
assert(!this.paused)
|
|
373
|
+
assert(request.upgrade || request.method === 'CONNECT')
|
|
374
|
+
|
|
375
|
+
this.statusCode = null
|
|
376
|
+
this.statusText = ''
|
|
377
|
+
this.shouldKeepAlive = null
|
|
378
|
+
|
|
379
|
+
assert(this.headers.length % 2 === 0)
|
|
380
|
+
this.headers = []
|
|
381
|
+
this.headersSize = 0
|
|
382
|
+
|
|
383
|
+
socket.unshift(head)
|
|
384
|
+
|
|
385
|
+
socket[kParser].destroy()
|
|
386
|
+
socket[kParser] = null
|
|
387
|
+
|
|
388
|
+
socket[kClient] = null
|
|
389
|
+
socket[kError] = null
|
|
390
|
+
|
|
391
|
+
removeAllListeners(socket)
|
|
392
|
+
|
|
393
|
+
client[kSocket] = null
|
|
394
|
+
client[kHTTPContext] = null // TODO (fix): This is hacky...
|
|
395
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
396
|
+
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
request.onUpgrade(statusCode, headers, socket)
|
|
400
|
+
} catch (err) {
|
|
401
|
+
util.destroy(socket, err)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
client[kResume]()
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
onHeadersComplete (statusCode, upgrade, shouldKeepAlive) {
|
|
408
|
+
const { client, socket, headers, statusText } = this
|
|
409
|
+
|
|
410
|
+
/* istanbul ignore next: difficult to make a test case for */
|
|
411
|
+
if (socket.destroyed) {
|
|
412
|
+
return -1
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
416
|
+
|
|
417
|
+
/* istanbul ignore next: difficult to make a test case for */
|
|
418
|
+
if (!request) {
|
|
419
|
+
return -1
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
assert(!this.upgrade)
|
|
423
|
+
assert(this.statusCode < 200)
|
|
424
|
+
|
|
425
|
+
if (statusCode === 100) {
|
|
426
|
+
util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
|
|
427
|
+
return -1
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* this can only happen if server is misbehaving */
|
|
431
|
+
if (upgrade && !request.upgrade) {
|
|
432
|
+
util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket)))
|
|
433
|
+
return -1
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS)
|
|
437
|
+
|
|
438
|
+
this.statusCode = statusCode
|
|
439
|
+
this.shouldKeepAlive = (
|
|
440
|
+
shouldKeepAlive ||
|
|
441
|
+
// Override llhttp value which does not allow keepAlive for HEAD.
|
|
442
|
+
(request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive')
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if (this.statusCode >= 200) {
|
|
446
|
+
const bodyTimeout = request.bodyTimeout != null
|
|
447
|
+
? request.bodyTimeout
|
|
448
|
+
: client[kBodyTimeout]
|
|
449
|
+
this.setTimeout(bodyTimeout, TIMEOUT_BODY)
|
|
450
|
+
} else if (this.timeout) {
|
|
451
|
+
// istanbul ignore else: only for jest
|
|
452
|
+
if (this.timeout.refresh) {
|
|
453
|
+
this.timeout.refresh()
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (request.method === 'CONNECT') {
|
|
458
|
+
assert(client[kRunning] === 1)
|
|
459
|
+
this.upgrade = true
|
|
460
|
+
return 2
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (upgrade) {
|
|
464
|
+
assert(client[kRunning] === 1)
|
|
465
|
+
this.upgrade = true
|
|
466
|
+
return 2
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
assert(this.headers.length % 2 === 0)
|
|
470
|
+
this.headers = []
|
|
471
|
+
this.headersSize = 0
|
|
472
|
+
|
|
473
|
+
if (this.shouldKeepAlive && client[kPipelining]) {
|
|
474
|
+
const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null
|
|
475
|
+
|
|
476
|
+
if (keepAliveTimeout != null) {
|
|
477
|
+
const timeout = Math.min(
|
|
478
|
+
keepAliveTimeout - client[kKeepAliveTimeoutThreshold],
|
|
479
|
+
client[kKeepAliveMaxTimeout]
|
|
480
|
+
)
|
|
481
|
+
if (timeout <= 0) {
|
|
482
|
+
socket[kReset] = true
|
|
483
|
+
} else {
|
|
484
|
+
client[kKeepAliveTimeoutValue] = timeout
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout]
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
// Stop more requests from being dispatched.
|
|
491
|
+
socket[kReset] = true
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
|
|
495
|
+
|
|
496
|
+
if (request.aborted) {
|
|
497
|
+
return -1
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (request.method === 'HEAD') {
|
|
501
|
+
return 1
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (statusCode < 200) {
|
|
505
|
+
return 1
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (socket[kBlocking]) {
|
|
509
|
+
socket[kBlocking] = false
|
|
510
|
+
client[kResume]()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return pause ? constants.ERROR.PAUSED : 0
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
onBody (buf) {
|
|
517
|
+
const { client, socket, statusCode, maxResponseSize } = this
|
|
518
|
+
|
|
519
|
+
if (socket.destroyed) {
|
|
520
|
+
return -1
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
524
|
+
assert(request)
|
|
525
|
+
|
|
526
|
+
assert.strictEqual(this.timeoutType, TIMEOUT_BODY)
|
|
527
|
+
if (this.timeout) {
|
|
528
|
+
// istanbul ignore else: only for jest
|
|
529
|
+
if (this.timeout.refresh) {
|
|
530
|
+
this.timeout.refresh()
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
assert(statusCode >= 200)
|
|
535
|
+
|
|
536
|
+
if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) {
|
|
537
|
+
util.destroy(socket, new ResponseExceededMaxSizeError())
|
|
538
|
+
return -1
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.bytesRead += buf.length
|
|
542
|
+
|
|
543
|
+
if (request.onData(buf) === false) {
|
|
544
|
+
return constants.ERROR.PAUSED
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
onMessageComplete () {
|
|
549
|
+
const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this
|
|
550
|
+
|
|
551
|
+
if (socket.destroyed && (!statusCode || shouldKeepAlive)) {
|
|
552
|
+
return -1
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (upgrade) {
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
560
|
+
assert(request)
|
|
561
|
+
|
|
562
|
+
assert(statusCode >= 100)
|
|
563
|
+
|
|
564
|
+
this.statusCode = null
|
|
565
|
+
this.statusText = ''
|
|
566
|
+
this.bytesRead = 0
|
|
567
|
+
this.contentLength = ''
|
|
568
|
+
this.keepAlive = ''
|
|
569
|
+
this.connection = ''
|
|
570
|
+
|
|
571
|
+
assert(this.headers.length % 2 === 0)
|
|
572
|
+
this.headers = []
|
|
573
|
+
this.headersSize = 0
|
|
574
|
+
|
|
575
|
+
if (statusCode < 200) {
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* istanbul ignore next: should be handled by llhttp? */
|
|
580
|
+
if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
|
|
581
|
+
util.destroy(socket, new ResponseContentLengthMismatchError())
|
|
582
|
+
return -1
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
request.onComplete(headers)
|
|
586
|
+
|
|
587
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
588
|
+
|
|
589
|
+
if (socket[kWriting]) {
|
|
590
|
+
assert.strictEqual(client[kRunning], 0)
|
|
591
|
+
// Response completed before request.
|
|
592
|
+
util.destroy(socket, new InformationalError('reset'))
|
|
593
|
+
return constants.ERROR.PAUSED
|
|
594
|
+
} else if (!shouldKeepAlive) {
|
|
595
|
+
util.destroy(socket, new InformationalError('reset'))
|
|
596
|
+
return constants.ERROR.PAUSED
|
|
597
|
+
} else if (socket[kReset] && client[kRunning] === 0) {
|
|
598
|
+
// Destroy socket once all requests have completed.
|
|
599
|
+
// The request at the tail of the pipeline is the one
|
|
600
|
+
// that requested reset and no further requests should
|
|
601
|
+
// have been queued since then.
|
|
602
|
+
util.destroy(socket, new InformationalError('reset'))
|
|
603
|
+
return constants.ERROR.PAUSED
|
|
604
|
+
} else if (client[kPipelining] == null || client[kPipelining] === 1) {
|
|
605
|
+
// We must wait a full event loop cycle to reuse this socket to make sure
|
|
606
|
+
// that non-spec compliant servers are not closing the connection even if they
|
|
607
|
+
// said they won't.
|
|
608
|
+
setImmediate(() => client[kResume]())
|
|
609
|
+
} else {
|
|
610
|
+
client[kResume]()
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function onParserTimeout (parser) {
|
|
616
|
+
const { socket, timeoutType, client } = parser
|
|
617
|
+
|
|
618
|
+
/* istanbul ignore else */
|
|
619
|
+
if (timeoutType === TIMEOUT_HEADERS) {
|
|
620
|
+
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
|
621
|
+
assert(!parser.paused, 'cannot be paused while waiting for headers')
|
|
622
|
+
util.destroy(socket, new HeadersTimeoutError())
|
|
623
|
+
}
|
|
624
|
+
} else if (timeoutType === TIMEOUT_BODY) {
|
|
625
|
+
if (!parser.paused) {
|
|
626
|
+
util.destroy(socket, new BodyTimeoutError())
|
|
627
|
+
}
|
|
628
|
+
} else if (timeoutType === TIMEOUT_IDLE) {
|
|
629
|
+
assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
|
|
630
|
+
util.destroy(socket, new InformationalError('socket idle timeout'))
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function connectH1 (client, socket) {
|
|
635
|
+
client[kSocket] = socket
|
|
636
|
+
|
|
637
|
+
if (!llhttpInstance) {
|
|
638
|
+
llhttpInstance = await llhttpPromise
|
|
639
|
+
llhttpPromise = null
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
socket[kNoRef] = false
|
|
643
|
+
socket[kWriting] = false
|
|
644
|
+
socket[kReset] = false
|
|
645
|
+
socket[kBlocking] = false
|
|
646
|
+
socket[kParser] = new Parser(client, socket, llhttpInstance)
|
|
647
|
+
|
|
648
|
+
addListener(socket, 'error', function (err) {
|
|
649
|
+
const parser = this[kParser]
|
|
650
|
+
|
|
651
|
+
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
652
|
+
|
|
653
|
+
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
|
|
654
|
+
// to the user.
|
|
655
|
+
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
|
|
656
|
+
// We treat all incoming data so for as a valid response.
|
|
657
|
+
parser.onMessageComplete()
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
this[kError] = err
|
|
662
|
+
|
|
663
|
+
this[kClient][kOnError](err)
|
|
664
|
+
})
|
|
665
|
+
addListener(socket, 'readable', function () {
|
|
666
|
+
const parser = this[kParser]
|
|
667
|
+
|
|
668
|
+
if (parser) {
|
|
669
|
+
parser.readMore()
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
addListener(socket, 'end', function () {
|
|
673
|
+
const parser = this[kParser]
|
|
674
|
+
|
|
675
|
+
if (parser.statusCode && !parser.shouldKeepAlive) {
|
|
676
|
+
// We treat all incoming data so far as a valid response.
|
|
677
|
+
parser.onMessageComplete()
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
|
|
682
|
+
})
|
|
683
|
+
addListener(socket, 'close', function () {
|
|
684
|
+
const client = this[kClient]
|
|
685
|
+
const parser = this[kParser]
|
|
686
|
+
|
|
687
|
+
if (parser) {
|
|
688
|
+
if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
|
|
689
|
+
// We treat all incoming data so far as a valid response.
|
|
690
|
+
parser.onMessageComplete()
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
this[kParser].destroy()
|
|
694
|
+
this[kParser] = null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
|
|
698
|
+
|
|
699
|
+
client[kSocket] = null
|
|
700
|
+
client[kHTTPContext] = null // TODO (fix): This is hacky...
|
|
701
|
+
|
|
702
|
+
if (client.destroyed) {
|
|
703
|
+
assert(client[kPending] === 0)
|
|
704
|
+
|
|
705
|
+
// Fail entire queue.
|
|
706
|
+
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
707
|
+
for (let i = 0; i < requests.length; i++) {
|
|
708
|
+
const request = requests[i]
|
|
709
|
+
errorRequest(client, request, err)
|
|
710
|
+
}
|
|
711
|
+
} else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
|
|
712
|
+
// Fail head of pipeline.
|
|
713
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
714
|
+
client[kQueue][client[kRunningIdx]++] = null
|
|
715
|
+
|
|
716
|
+
errorRequest(client, request, err)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
720
|
+
|
|
721
|
+
assert(client[kRunning] === 0)
|
|
722
|
+
|
|
723
|
+
client.emit('disconnect', client[kUrl], [client], err)
|
|
724
|
+
|
|
725
|
+
client[kResume]()
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
let closed = false
|
|
729
|
+
socket.on('close', () => {
|
|
730
|
+
closed = true
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
version: 'h1',
|
|
735
|
+
defaultPipelining: 1,
|
|
736
|
+
write (...args) {
|
|
737
|
+
return writeH1(client, ...args)
|
|
738
|
+
},
|
|
739
|
+
resume () {
|
|
740
|
+
resumeH1(client)
|
|
741
|
+
},
|
|
742
|
+
destroy (err, callback) {
|
|
743
|
+
if (closed) {
|
|
744
|
+
queueMicrotask(callback)
|
|
745
|
+
} else {
|
|
746
|
+
socket.destroy(err).on('close', callback)
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
get destroyed () {
|
|
750
|
+
return socket.destroyed
|
|
751
|
+
},
|
|
752
|
+
busy (request) {
|
|
753
|
+
if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
|
|
754
|
+
return true
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (request) {
|
|
758
|
+
if (client[kRunning] > 0 && !request.idempotent) {
|
|
759
|
+
// Non-idempotent request cannot be retried.
|
|
760
|
+
// Ensure that no other requests are inflight and
|
|
761
|
+
// could cause failure.
|
|
762
|
+
return true
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) {
|
|
766
|
+
// Don't dispatch an upgrade until all preceding requests have completed.
|
|
767
|
+
// A misbehaving server might upgrade the connection before all pipelined
|
|
768
|
+
// request has completed.
|
|
769
|
+
return true
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 &&
|
|
773
|
+
(util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) {
|
|
774
|
+
// Request with stream or iterator body can error while other requests
|
|
775
|
+
// are inflight and indirectly error those as well.
|
|
776
|
+
// Ensure this doesn't happen by waiting for inflight
|
|
777
|
+
// to complete before dispatching.
|
|
778
|
+
|
|
779
|
+
// Request with stream or iterator body cannot be retried.
|
|
780
|
+
// Ensure that no other requests are inflight and
|
|
781
|
+
// could cause failure.
|
|
782
|
+
return true
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return false
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function resumeH1 (client) {
|
|
792
|
+
const socket = client[kSocket]
|
|
793
|
+
|
|
794
|
+
if (socket && !socket.destroyed) {
|
|
795
|
+
if (client[kSize] === 0) {
|
|
796
|
+
if (!socket[kNoRef] && socket.unref) {
|
|
797
|
+
socket.unref()
|
|
798
|
+
socket[kNoRef] = true
|
|
799
|
+
}
|
|
800
|
+
} else if (socket[kNoRef] && socket.ref) {
|
|
801
|
+
socket.ref()
|
|
802
|
+
socket[kNoRef] = false
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (client[kSize] === 0) {
|
|
806
|
+
if (socket[kParser].timeoutType !== TIMEOUT_IDLE) {
|
|
807
|
+
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE)
|
|
808
|
+
}
|
|
809
|
+
} else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
|
|
810
|
+
if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
|
|
811
|
+
const request = client[kQueue][client[kRunningIdx]]
|
|
812
|
+
const headersTimeout = request.headersTimeout != null
|
|
813
|
+
? request.headersTimeout
|
|
814
|
+
: client[kHeadersTimeout]
|
|
815
|
+
socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function errorRequest (client, request, err) {
|
|
822
|
+
try {
|
|
823
|
+
request.onError(err)
|
|
824
|
+
assert(request.aborted)
|
|
825
|
+
} catch (err) {
|
|
826
|
+
client.emit('error', err)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
|
|
831
|
+
function shouldSendContentLength (method) {
|
|
832
|
+
return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function writeH1 (client, request) {
|
|
836
|
+
const { method, path, host, upgrade, blocking, reset } = request
|
|
837
|
+
|
|
838
|
+
let { body, headers, contentLength } = request
|
|
839
|
+
|
|
840
|
+
// https://tools.ietf.org/html/rfc7231#section-4.3.1
|
|
841
|
+
// https://tools.ietf.org/html/rfc7231#section-4.3.2
|
|
842
|
+
// https://tools.ietf.org/html/rfc7231#section-4.3.5
|
|
843
|
+
|
|
844
|
+
// Sending a payload body on a request that does not
|
|
845
|
+
// expect it can cause undefined behavior on some
|
|
846
|
+
// servers and corrupt connection state. Do not
|
|
847
|
+
// re-use the connection for further requests.
|
|
848
|
+
|
|
849
|
+
const expectsPayload = (
|
|
850
|
+
method === 'PUT' ||
|
|
851
|
+
method === 'POST' ||
|
|
852
|
+
method === 'PATCH'
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if (util.isFormDataLike(body)) {
|
|
856
|
+
if (!extractBody) {
|
|
857
|
+
extractBody = require('../web/fetch/body.js').extractBody
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const [bodyStream, contentType] = extractBody(body)
|
|
861
|
+
if (request.contentType == null) {
|
|
862
|
+
headers.push('content-type', contentType)
|
|
863
|
+
}
|
|
864
|
+
body = bodyStream.stream
|
|
865
|
+
contentLength = bodyStream.length
|
|
866
|
+
} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
|
|
867
|
+
headers.push('content-type', body.type)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (body && typeof body.read === 'function') {
|
|
871
|
+
// Try to read EOF in order to get length.
|
|
872
|
+
body.read(0)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const bodyLength = util.bodyLength(body)
|
|
876
|
+
|
|
877
|
+
contentLength = bodyLength ?? contentLength
|
|
878
|
+
|
|
879
|
+
if (contentLength === null) {
|
|
880
|
+
contentLength = request.contentLength
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (contentLength === 0 && !expectsPayload) {
|
|
884
|
+
// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
|
885
|
+
// A user agent SHOULD NOT send a Content-Length header field when
|
|
886
|
+
// the request message does not contain a payload body and the method
|
|
887
|
+
// semantics do not anticipate such a body.
|
|
888
|
+
|
|
889
|
+
contentLength = null
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// https://github.com/nodejs/undici/issues/2046
|
|
893
|
+
// A user agent may send a Content-Length header with 0 value, this should be allowed.
|
|
894
|
+
if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
|
|
895
|
+
if (client[kStrictContentLength]) {
|
|
896
|
+
errorRequest(client, request, new RequestContentLengthMismatchError())
|
|
897
|
+
return false
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
process.emitWarning(new RequestContentLengthMismatchError())
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const socket = client[kSocket]
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
request.onConnect((err) => {
|
|
907
|
+
if (request.aborted || request.completed) {
|
|
908
|
+
return
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
errorRequest(client, request, err || new RequestAbortedError())
|
|
912
|
+
|
|
913
|
+
util.destroy(socket, new InformationalError('aborted'))
|
|
914
|
+
})
|
|
915
|
+
} catch (err) {
|
|
916
|
+
errorRequest(client, request, err)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (request.aborted) {
|
|
920
|
+
util.destroy(body)
|
|
921
|
+
return false
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (method === 'HEAD') {
|
|
925
|
+
// https://github.com/mcollina/undici/issues/258
|
|
926
|
+
// Close after a HEAD request to interop with misbehaving servers
|
|
927
|
+
// that may send a body in the response.
|
|
928
|
+
|
|
929
|
+
socket[kReset] = true
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (upgrade || method === 'CONNECT') {
|
|
933
|
+
// On CONNECT or upgrade, block pipeline from dispatching further
|
|
934
|
+
// requests on this connection.
|
|
935
|
+
|
|
936
|
+
socket[kReset] = true
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (reset != null) {
|
|
940
|
+
socket[kReset] = reset
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) {
|
|
944
|
+
socket[kReset] = true
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (blocking) {
|
|
948
|
+
socket[kBlocking] = true
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
let header = `${method} ${path} HTTP/1.1\r\n`
|
|
952
|
+
|
|
953
|
+
if (typeof host === 'string') {
|
|
954
|
+
header += `host: ${host}\r\n`
|
|
955
|
+
} else {
|
|
956
|
+
header += client[kHostHeader]
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (upgrade) {
|
|
960
|
+
header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n`
|
|
961
|
+
} else if (client[kPipelining] && !socket[kReset]) {
|
|
962
|
+
header += 'connection: keep-alive\r\n'
|
|
963
|
+
} else {
|
|
964
|
+
header += 'connection: close\r\n'
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (Array.isArray(headers)) {
|
|
968
|
+
for (let n = 0; n < headers.length; n += 2) {
|
|
969
|
+
const key = headers[n + 0]
|
|
970
|
+
const val = headers[n + 1]
|
|
971
|
+
|
|
972
|
+
if (Array.isArray(val)) {
|
|
973
|
+
for (let i = 0; i < val.length; i++) {
|
|
974
|
+
header += `${key}: ${val[i]}\r\n`
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
header += `${key}: ${val}\r\n`
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (channels.sendHeaders.hasSubscribers) {
|
|
983
|
+
channels.sendHeaders.publish({ request, headers: header, socket })
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* istanbul ignore else: assertion */
|
|
987
|
+
if (!body || bodyLength === 0) {
|
|
988
|
+
if (contentLength === 0) {
|
|
989
|
+
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
|
|
990
|
+
} else {
|
|
991
|
+
assert(contentLength === null, 'no body must not have content length')
|
|
992
|
+
socket.write(`${header}\r\n`, 'latin1')
|
|
993
|
+
}
|
|
994
|
+
request.onRequestSent()
|
|
995
|
+
} else if (util.isBuffer(body)) {
|
|
996
|
+
assert(contentLength === body.byteLength, 'buffer body must have content length')
|
|
997
|
+
|
|
998
|
+
socket.cork()
|
|
999
|
+
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
|
|
1000
|
+
socket.write(body)
|
|
1001
|
+
socket.uncork()
|
|
1002
|
+
request.onBodySent(body)
|
|
1003
|
+
request.onRequestSent()
|
|
1004
|
+
if (!expectsPayload) {
|
|
1005
|
+
socket[kReset] = true
|
|
1006
|
+
}
|
|
1007
|
+
} else if (util.isBlobLike(body)) {
|
|
1008
|
+
if (typeof body.stream === 'function') {
|
|
1009
|
+
writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload })
|
|
1010
|
+
} else {
|
|
1011
|
+
writeBlob({ body, client, request, socket, contentLength, header, expectsPayload })
|
|
1012
|
+
}
|
|
1013
|
+
} else if (util.isStream(body)) {
|
|
1014
|
+
writeStream({ body, client, request, socket, contentLength, header, expectsPayload })
|
|
1015
|
+
} else if (util.isIterable(body)) {
|
|
1016
|
+
writeIterable({ body, client, request, socket, contentLength, header, expectsPayload })
|
|
1017
|
+
} else {
|
|
1018
|
+
assert(false)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return true
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1025
|
+
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
|
|
1026
|
+
|
|
1027
|
+
let finished = false
|
|
1028
|
+
|
|
1029
|
+
const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
|
|
1030
|
+
|
|
1031
|
+
const onData = function (chunk) {
|
|
1032
|
+
if (finished) {
|
|
1033
|
+
return
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
try {
|
|
1037
|
+
if (!writer.write(chunk) && this.pause) {
|
|
1038
|
+
this.pause()
|
|
1039
|
+
}
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
util.destroy(this, err)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const onDrain = function () {
|
|
1045
|
+
if (finished) {
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (body.resume) {
|
|
1050
|
+
body.resume()
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const onClose = function () {
|
|
1054
|
+
// 'close' might be emitted *before* 'error' for
|
|
1055
|
+
// broken streams. Wait a tick to avoid this case.
|
|
1056
|
+
queueMicrotask(() => {
|
|
1057
|
+
// It's only safe to remove 'error' listener after
|
|
1058
|
+
// 'close'.
|
|
1059
|
+
body.removeListener('error', onFinished)
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
if (!finished) {
|
|
1063
|
+
const err = new RequestAbortedError()
|
|
1064
|
+
queueMicrotask(() => onFinished(err))
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const onFinished = function (err) {
|
|
1068
|
+
if (finished) {
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
finished = true
|
|
1073
|
+
|
|
1074
|
+
assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1))
|
|
1075
|
+
|
|
1076
|
+
socket
|
|
1077
|
+
.off('drain', onDrain)
|
|
1078
|
+
.off('error', onFinished)
|
|
1079
|
+
|
|
1080
|
+
body
|
|
1081
|
+
.removeListener('data', onData)
|
|
1082
|
+
.removeListener('end', onFinished)
|
|
1083
|
+
.removeListener('close', onClose)
|
|
1084
|
+
|
|
1085
|
+
if (!err) {
|
|
1086
|
+
try {
|
|
1087
|
+
writer.end()
|
|
1088
|
+
} catch (er) {
|
|
1089
|
+
err = er
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
writer.destroy(err)
|
|
1094
|
+
|
|
1095
|
+
if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) {
|
|
1096
|
+
util.destroy(body, err)
|
|
1097
|
+
} else {
|
|
1098
|
+
util.destroy(body)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
body
|
|
1103
|
+
.on('data', onData)
|
|
1104
|
+
.on('end', onFinished)
|
|
1105
|
+
.on('error', onFinished)
|
|
1106
|
+
.on('close', onClose)
|
|
1107
|
+
|
|
1108
|
+
if (body.resume) {
|
|
1109
|
+
body.resume()
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
socket
|
|
1113
|
+
.on('drain', onDrain)
|
|
1114
|
+
.on('error', onFinished)
|
|
1115
|
+
|
|
1116
|
+
if (body.errorEmitted ?? body.errored) {
|
|
1117
|
+
setImmediate(() => onFinished(body.errored))
|
|
1118
|
+
} else if (body.endEmitted ?? body.readableEnded) {
|
|
1119
|
+
setImmediate(() => onFinished(null))
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (body.closeEmitted ?? body.closed) {
|
|
1123
|
+
setImmediate(onClose)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1128
|
+
assert(contentLength === body.size, 'blob body must have content length')
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
if (contentLength != null && contentLength !== body.size) {
|
|
1132
|
+
throw new RequestContentLengthMismatchError()
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const buffer = Buffer.from(await body.arrayBuffer())
|
|
1136
|
+
|
|
1137
|
+
socket.cork()
|
|
1138
|
+
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
|
|
1139
|
+
socket.write(buffer)
|
|
1140
|
+
socket.uncork()
|
|
1141
|
+
|
|
1142
|
+
request.onBodySent(buffer)
|
|
1143
|
+
request.onRequestSent()
|
|
1144
|
+
|
|
1145
|
+
if (!expectsPayload) {
|
|
1146
|
+
socket[kReset] = true
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
client[kResume]()
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
util.destroy(socket, err)
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
|
|
1156
|
+
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
|
|
1157
|
+
|
|
1158
|
+
let callback = null
|
|
1159
|
+
function onDrain () {
|
|
1160
|
+
if (callback) {
|
|
1161
|
+
const cb = callback
|
|
1162
|
+
callback = null
|
|
1163
|
+
cb()
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const waitForDrain = () => new Promise((resolve, reject) => {
|
|
1168
|
+
assert(callback === null)
|
|
1169
|
+
|
|
1170
|
+
if (socket[kError]) {
|
|
1171
|
+
reject(socket[kError])
|
|
1172
|
+
} else {
|
|
1173
|
+
callback = resolve
|
|
1174
|
+
}
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
socket
|
|
1178
|
+
.on('close', onDrain)
|
|
1179
|
+
.on('drain', onDrain)
|
|
1180
|
+
|
|
1181
|
+
const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
|
|
1182
|
+
try {
|
|
1183
|
+
// It's up to the user to somehow abort the async iterable.
|
|
1184
|
+
for await (const chunk of body) {
|
|
1185
|
+
if (socket[kError]) {
|
|
1186
|
+
throw socket[kError]
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (!writer.write(chunk)) {
|
|
1190
|
+
await waitForDrain()
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
writer.end()
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
writer.destroy(err)
|
|
1197
|
+
} finally {
|
|
1198
|
+
socket
|
|
1199
|
+
.off('close', onDrain)
|
|
1200
|
+
.off('drain', onDrain)
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
class AsyncWriter {
|
|
1205
|
+
constructor ({ socket, request, contentLength, client, expectsPayload, header }) {
|
|
1206
|
+
this.socket = socket
|
|
1207
|
+
this.request = request
|
|
1208
|
+
this.contentLength = contentLength
|
|
1209
|
+
this.client = client
|
|
1210
|
+
this.bytesWritten = 0
|
|
1211
|
+
this.expectsPayload = expectsPayload
|
|
1212
|
+
this.header = header
|
|
1213
|
+
|
|
1214
|
+
socket[kWriting] = true
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
write (chunk) {
|
|
1218
|
+
const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this
|
|
1219
|
+
|
|
1220
|
+
if (socket[kError]) {
|
|
1221
|
+
throw socket[kError]
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (socket.destroyed) {
|
|
1225
|
+
return false
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const len = Buffer.byteLength(chunk)
|
|
1229
|
+
if (!len) {
|
|
1230
|
+
return true
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// We should defer writing chunks.
|
|
1234
|
+
if (contentLength !== null && bytesWritten + len > contentLength) {
|
|
1235
|
+
if (client[kStrictContentLength]) {
|
|
1236
|
+
throw new RequestContentLengthMismatchError()
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
process.emitWarning(new RequestContentLengthMismatchError())
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
socket.cork()
|
|
1243
|
+
|
|
1244
|
+
if (bytesWritten === 0) {
|
|
1245
|
+
if (!expectsPayload) {
|
|
1246
|
+
socket[kReset] = true
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (contentLength === null) {
|
|
1250
|
+
socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1')
|
|
1251
|
+
} else {
|
|
1252
|
+
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (contentLength === null) {
|
|
1257
|
+
socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1')
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
this.bytesWritten += len
|
|
1261
|
+
|
|
1262
|
+
const ret = socket.write(chunk)
|
|
1263
|
+
|
|
1264
|
+
socket.uncork()
|
|
1265
|
+
|
|
1266
|
+
request.onBodySent(chunk)
|
|
1267
|
+
|
|
1268
|
+
if (!ret) {
|
|
1269
|
+
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1270
|
+
// istanbul ignore else: only for jest
|
|
1271
|
+
if (socket[kParser].timeout.refresh) {
|
|
1272
|
+
socket[kParser].timeout.refresh()
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return ret
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
end () {
|
|
1281
|
+
const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this
|
|
1282
|
+
request.onRequestSent()
|
|
1283
|
+
|
|
1284
|
+
socket[kWriting] = false
|
|
1285
|
+
|
|
1286
|
+
if (socket[kError]) {
|
|
1287
|
+
throw socket[kError]
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (socket.destroyed) {
|
|
1291
|
+
return
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (bytesWritten === 0) {
|
|
1295
|
+
if (expectsPayload) {
|
|
1296
|
+
// https://tools.ietf.org/html/rfc7230#section-3.3.2
|
|
1297
|
+
// A user agent SHOULD send a Content-Length in a request message when
|
|
1298
|
+
// no Transfer-Encoding is sent and the request method defines a meaning
|
|
1299
|
+
// for an enclosed payload body.
|
|
1300
|
+
|
|
1301
|
+
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
|
|
1302
|
+
} else {
|
|
1303
|
+
socket.write(`${header}\r\n`, 'latin1')
|
|
1304
|
+
}
|
|
1305
|
+
} else if (contentLength === null) {
|
|
1306
|
+
socket.write('\r\n0\r\n\r\n', 'latin1')
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (contentLength !== null && bytesWritten !== contentLength) {
|
|
1310
|
+
if (client[kStrictContentLength]) {
|
|
1311
|
+
throw new RequestContentLengthMismatchError()
|
|
1312
|
+
} else {
|
|
1313
|
+
process.emitWarning(new RequestContentLengthMismatchError())
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
|
|
1318
|
+
// istanbul ignore else: only for jest
|
|
1319
|
+
if (socket[kParser].timeout.refresh) {
|
|
1320
|
+
socket[kParser].timeout.refresh()
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
client[kResume]()
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
destroy (err) {
|
|
1328
|
+
const { socket, client } = this
|
|
1329
|
+
|
|
1330
|
+
socket[kWriting] = false
|
|
1331
|
+
|
|
1332
|
+
if (err) {
|
|
1333
|
+
assert(client[kRunning] <= 1, 'pipeline should only contain this request')
|
|
1334
|
+
util.destroy(socket, err)
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
module.exports = connectH1
|