| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060 |
- 'use strict'
- const assert = require('node:assert')
- const { pipeline } = require('node:stream')
- const util = require('../core/util.js')
- const {
- RequestContentLengthMismatchError,
- RequestAbortedError,
- SocketError,
- InformationalError,
- InvalidArgumentError,
- HeadersTimeoutError,
- BodyTimeoutError
- } = require('../core/errors.js')
- const {
- kUrl,
- kReset,
- kClient,
- kRunning,
- kPending,
- kQueue,
- kPendingIdx,
- kRunningIdx,
- kError,
- kSocket,
- kStrictContentLength,
- kOnError,
- kMaxConcurrentStreams,
- kPingInterval,
- kHTTP2Session,
- kHTTP2InitialWindowSize,
- kHTTP2ConnectionWindowSize,
- kResume,
- kSize,
- kHTTPContext,
- kClosed,
- kBodyTimeout,
- kHeadersTimeout,
- kEnableConnectProtocol,
- kRemoteSettings,
- kHTTP2Stream,
- kHTTP2SessionState
- } = require('../core/symbols.js')
- const { channels } = require('../core/diagnostics.js')
- const kOpenStreams = Symbol('open streams')
- let extractBody
- /** @type {import('http2')} */
- let http2
- try {
- http2 = require('node:http2')
- } catch {
- // @ts-ignore
- http2 = { constants: {} }
- }
- const {
- constants: {
- HTTP2_HEADER_AUTHORITY,
- HTTP2_HEADER_METHOD,
- HTTP2_HEADER_PATH,
- HTTP2_HEADER_SCHEME,
- HTTP2_HEADER_CONTENT_LENGTH,
- HTTP2_HEADER_EXPECT,
- HTTP2_HEADER_STATUS,
- HTTP2_HEADER_PROTOCOL,
- NGHTTP2_REFUSED_STREAM,
- NGHTTP2_CANCEL
- }
- } = http2
- function parseH2Headers (headers) {
- const result = []
- for (const [name, value] of Object.entries(headers)) {
- // h2 may concat the header value by array
- // e.g. Set-Cookie
- if (Array.isArray(value)) {
- for (const subvalue of value) {
- // we need to provide each header value of header name
- // because the headers handler expect name-value pair
- result.push(Buffer.from(name), Buffer.from(subvalue))
- }
- } else {
- result.push(Buffer.from(name), Buffer.from(value))
- }
- }
- return result
- }
- function connectH2 (client, socket) {
- client[kSocket] = socket
- const http2InitialWindowSize = client[kHTTP2InitialWindowSize]
- const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]
- const session = http2.connect(client[kUrl], {
- createConnection: () => socket,
- peerMaxConcurrentStreams: client[kMaxConcurrentStreams],
- settings: {
- // TODO(metcoder95): add support for PUSH
- enablePush: false,
- ...(http2InitialWindowSize != null ? { initialWindowSize: http2InitialWindowSize } : null)
- }
- })
- client[kSocket] = socket
- session[kOpenStreams] = 0
- session[kClient] = client
- session[kSocket] = socket
- session[kHTTP2SessionState] = {
- ping: {
- interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
- }
- }
- // We set it to true by default in a best-effort; however once connected to an H2 server
- // we will check if extended CONNECT protocol is supported or not
- // and set this value accordingly.
- session[kEnableConnectProtocol] = false
- // States whether or not we have received the remote settings from the server
- session[kRemoteSettings] = false
- // Apply connection-level flow control once connected (if supported).
- if (http2ConnectionWindowSize) {
- util.addListener(session, 'connect', applyConnectionWindowSize.bind(session, http2ConnectionWindowSize))
- }
- util.addListener(session, 'error', onHttp2SessionError)
- util.addListener(session, 'frameError', onHttp2FrameError)
- util.addListener(session, 'end', onHttp2SessionEnd)
- util.addListener(session, 'goaway', onHttp2SessionGoAway)
- util.addListener(session, 'close', onHttp2SessionClose)
- util.addListener(session, 'remoteSettings', onHttp2RemoteSettings)
- // TODO (@metcoder95): implement SETTINGS support
- // util.addListener(session, 'localSettings', onHttp2RemoteSettings)
- session.unref()
- client[kHTTP2Session] = session
- socket[kHTTP2Session] = session
- util.addListener(socket, 'error', onHttp2SocketError)
- util.addListener(socket, 'end', onHttp2SocketEnd)
- util.addListener(socket, 'close', onHttp2SocketClose)
- socket[kClosed] = false
- socket.on('close', onSocketClose)
- return {
- version: 'h2',
- defaultPipelining: Infinity,
- /**
- * @param {import('../core/request.js')} request
- * @returns {boolean}
- */
- write (request) {
- return writeH2(client, request)
- },
- /**
- * @returns {void}
- */
- resume () {
- resumeH2(client)
- },
- /**
- * @param {Error | null} err
- * @param {() => void} callback
- */
- destroy (err, callback) {
- if (socket[kClosed]) {
- queueMicrotask(callback)
- } else {
- socket.destroy(err).on('close', callback)
- }
- },
- /**
- * @type {boolean}
- */
- get destroyed () {
- return socket.destroyed
- },
- /**
- * @param {import('../core/request.js')} request
- * @returns {boolean}
- */
- busy (request) {
- if (request != null) {
- if (client[kRunning] > 0) {
- // We are already processing requests
- // Non-idempotent request cannot be retried.
- // Ensure that no other requests are inflight and
- // could cause failure.
- if (request.idempotent === false) return true
- // Don't dispatch an upgrade until all preceding requests have completed.
- // Possibly, we do not have remote settings confirmed yet.
- if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
- // Request with stream or iterator body can error while other requests
- // are inflight and indirectly error those as well.
- // Ensure this doesn't happen by waiting for inflight
- // to complete before dispatching.
- // Request with stream or iterator body cannot be retried.
- // Ensure that no other requests are inflight and
- // could cause failure.
- if (util.bodyLength(request.body) !== 0 &&
- (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true
- } else {
- return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false
- }
- }
- return false
- }
- }
- }
- function resumeH2 (client) {
- const socket = client[kSocket]
- if (socket?.destroyed === false) {
- // Only let the process exit when there is genuinely nothing outstanding.
- // Unreffing because the peer advertised MAX_CONCURRENT_STREAMS = 0 left
- // queued requests with nothing holding the event loop open, so the process
- // could exit with status 0 while an awaited request never settled.
- if (client[kSize] === 0) {
- socket.unref()
- client[kHTTP2Session].unref()
- } else {
- socket.ref()
- client[kHTTP2Session].ref()
- }
- }
- }
- function applyConnectionWindowSize (connectionWindowSize) {
- try {
- if (typeof this.setLocalWindowSize === 'function') {
- this.setLocalWindowSize(connectionWindowSize)
- }
- } catch {
- // Best-effort only.
- }
- }
- function onHttp2RemoteSettings (settings) {
- // Fallbacks are a safe bet, remote setting will always override
- this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams]
- /**
- * From RFC-8441
- * A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter
- * with the value of 0 after previously sending a value of 1.
- */
- // Note: Cannot be tested in Node, it does not supports disabling the extended CONNECT protocol once enabled
- if (this[kRemoteSettings] === true && this[kEnableConnectProtocol] === true && settings.enableConnectProtocol === false) {
- const err = new InformationalError('HTTP/2: Server disabled extended CONNECT protocol against RFC-8441')
- this[kSocket][kError] = err
- this[kClient][kOnError](err)
- return
- }
- this[kEnableConnectProtocol] = settings.enableConnectProtocol ?? this[kEnableConnectProtocol]
- this[kRemoteSettings] = true
- this[kClient][kResume]()
- }
- function onHttp2SendPing (session) {
- const state = session[kHTTP2SessionState]
- if ((session.closed || session.destroyed) && state.ping.interval != null) {
- clearInterval(state.ping.interval)
- state.ping.interval = null
- return
- }
- // If no ping sent, do nothing
- session.ping(onPing.bind(session))
- function onPing (err, duration) {
- const client = this[kClient]
- const socket = this[kClient]
- if (err != null) {
- const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
- socket[kError] = error
- client[kOnError](error)
- } else {
- client.emit('ping', duration)
- }
- }
- }
- function onHttp2SessionError (err) {
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
- this[kSocket][kError] = err
- this[kClient][kOnError](err)
- }
- function onHttp2FrameError (type, code, id) {
- if (id === 0) {
- const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
- this[kSocket][kError] = err
- this[kClient][kOnError](err)
- }
- }
- function onHttp2SessionEnd () {
- const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket]))
- this.destroy(err)
- util.destroy(this[kSocket], err)
- }
- /**
- * This is the root cause of #3011
- * We need to handle GOAWAY frames properly, and trigger the session close
- * along with the socket right away
- *
- * @this {import('http2').ClientHttp2Session}
- * @param {number} errorCode
- */
- // Backport of #5410 and #5569. HTTP/2 multiplexes, so requests complete out of
- // order; advancing kRunningIdx blindly retired whichever request happened to
- // sit at the head instead of the one that actually finished, which both lost
- // requests and left phantom running slots behind.
- function completeRequest (client, request, resetPendingIdx = false) {
- const queue = client[kQueue]
- const runningIdx = client[kRunningIdx]
- // In-order completion: clear the request and advance without splicing.
- // The client's resume loop compacts cleared slots once the index grows.
- if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
- queue[runningIdx] = null
- client[kRunningIdx] = runningIdx + 1
- return
- }
- const index = queue.indexOf(request, runningIdx)
- if (index === -1 || index >= client[kPendingIdx]) {
- return
- }
- queue.splice(index, 1)
- client[kPendingIdx]--
- if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
- client[kPendingIdx] = client[kRunningIdx]
- }
- }
- function onHttp2SessionGoAway (errorCode) {
- // TODO(mcollina): Verify if GOAWAY implements the spec correctly:
- // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
- // Specifically, we do not verify the "valid" stream id.
- const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
- const client = this[kClient]
- client[kSocket] = null
- client[kHTTPContext] = null
- // this is an HTTP2 session
- this.close()
- this[kHTTP2Session] = null
- util.destroy(this[kSocket], err)
- // Fail head of pipeline.
- if (client[kRunningIdx] < client[kQueue].length) {
- const request = client[kQueue][client[kRunningIdx]]
- client[kQueue][client[kRunningIdx]++] = null
- if (request != null) {
- util.errorRequest(client, request, err)
- }
- client[kPendingIdx] = client[kRunningIdx]
- }
- assert(client[kRunning] === 0)
- client.emit('disconnect', client[kUrl], [client], err)
- client.emit('connectionError', client[kUrl], [client], err)
- client[kResume]()
- }
- function onHttp2SessionClose () {
- const { [kClient]: client, [kHTTP2SessionState]: state } = this
- const { [kSocket]: socket } = client
- const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
- client[kSocket] = null
- client[kHTTPContext] = null
- if (state.ping.interval != null) {
- clearInterval(state.ping.interval)
- state.ping.interval = null
- }
- if (client.destroyed) {
- assert(client[kPending] === 0)
- // Fail entire queue.
- const requests = client[kQueue].splice(client[kRunningIdx])
- for (let i = 0; i < requests.length; i++) {
- const request = requests[i]
- if (request != null) {
- util.errorRequest(client, request, err)
- }
- }
- }
- }
- function onHttp2SocketClose () {
- const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
- const client = this[kHTTP2Session][kClient]
- client[kSocket] = null
- client[kHTTPContext] = null
- if (this[kHTTP2Session] !== null) {
- this[kHTTP2Session].destroy(err)
- }
- client[kPendingIdx] = client[kRunningIdx]
- assert(client[kRunning] === 0)
- client.emit('disconnect', client[kUrl], [client], err)
- client[kResume]()
- }
- function onHttp2SocketError (err) {
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
- this[kError] = err
- this[kClient][kOnError](err)
- }
- function onHttp2SocketEnd () {
- util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
- }
- function onSocketClose () {
- this[kClosed] = true
- }
- // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
- function shouldSendContentLength (method) {
- return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
- }
- function writeH2 (client, request) {
- // Time to the response headers, then time between body chunks. Using
- // bodyTimeout for both made headersTimeout a no-op over HTTP/2.
- const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
- const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
- const session = client[kHTTP2Session]
- const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
- let { body } = request
- if (upgrade != null && upgrade !== 'websocket') {
- util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
- return false
- }
- const headers = {}
- for (let n = 0; n < reqHeaders.length; n += 2) {
- const key = reqHeaders[n + 0]
- const val = reqHeaders[n + 1]
- if (key === 'cookie') {
- if (headers[key] != null) {
- headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
- } else {
- headers[key] = val
- }
- continue
- }
- if (Array.isArray(val)) {
- for (let i = 0; i < val.length; i++) {
- if (headers[key]) {
- headers[key] += `, ${val[i]}`
- } else {
- headers[key] = val[i]
- }
- }
- } else if (headers[key]) {
- headers[key] += `, ${val}`
- } else {
- headers[key] = val
- }
- }
- /** @type {import('node:http2').ClientHttp2Stream} */
- let stream = null
- const { hostname, port } = client[kUrl]
- headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
- headers[HTTP2_HEADER_METHOD] = method
- const abort = (err) => {
- if (request.aborted || request.completed) {
- return
- }
- err = err || new RequestAbortedError()
- util.errorRequest(client, request, err)
- if (stream != null) {
- // Some chunks might still come after abort,
- // let's ignore them
- stream.removeAllListeners('data')
- // On Abort, we close the stream to send RST_STREAM frame
- stream.close()
- // We move the running index to the next request
- client[kOnError](err)
- completeRequest(client, request)
- client[kResume]()
- }
- // We do not destroy the socket as we can continue using the session
- // the stream gets destroyed and the session remains to create new streams
- util.destroy(body, err)
- }
- try {
- // We are already connected, streams are pending.
- // We can call on connect, and wait for abort
- request.onConnect(abort)
- } catch (err) {
- util.errorRequest(client, request, err)
- }
- if (request.aborted) {
- return false
- }
- if (upgrade || method === 'CONNECT') {
- session.ref()
- if (upgrade === 'websocket') {
- // We cannot upgrade to websocket if extended CONNECT protocol is not supported
- if (session[kEnableConnectProtocol] === false) {
- util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
- session.unref()
- return false
- }
- // We force the method to CONNECT
- // as per RFC-8441
- // https://datatracker.ietf.org/doc/html/rfc8441#section-4
- headers[HTTP2_HEADER_METHOD] = 'CONNECT'
- headers[HTTP2_HEADER_PROTOCOL] = 'websocket'
- // :path and :scheme headers must be omitted when sending CONNECT but set if extended-CONNECT
- headers[HTTP2_HEADER_PATH] = path
- if (protocol === 'ws:' || protocol === 'wss:') {
- headers[HTTP2_HEADER_SCHEME] = protocol === 'ws:' ? 'http' : 'https'
- } else {
- headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
- }
- stream = session.request(headers, { endStream: false, signal })
- stream[kHTTP2Stream] = true
- ++session[kOpenStreams]
- stream.once('response', (headers, _flags) => {
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
- try {
- request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
- } catch (err) {
- abort(err)
- return
- }
- completeRequest(client, request)
- })
- stream.on('error', () => {
- if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) {
- // NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8)
- // We do not treat those as errors as the server might
- // not support websockets and refuse the stream
- abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
- }
- })
- stream.once('close', () => {
- session[kOpenStreams] -= 1
- if (session[kOpenStreams] === 0) session.unref()
- })
- stream.setTimeout(headersTimeout)
- return true
- }
- // TODO: consolidate once we support CONNECT properly
- // NOTE: We are already connected, streams are pending, first request
- // will create a new stream. We trigger a request to create the stream and wait until
- // `ready` event is triggered
- // We disabled endStream to allow the user to write to the stream
- stream = session.request(headers, { endStream: false, signal })
- stream[kHTTP2Stream] = true
- ++session[kOpenStreams]
- stream.on('response', headers => {
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
- try {
- request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
- } catch (err) {
- abort(err)
- return
- }
- completeRequest(client, request)
- })
- stream.on('error', abort)
- stream.once('close', () => {
- session[kOpenStreams] -= 1
- if (session[kOpenStreams] === 0) session.unref()
- })
- stream.setTimeout(headersTimeout)
- return true
- }
- // https://tools.ietf.org/html/rfc7540#section-8.3
- // :path and :scheme headers must be omitted when sending CONNECT
- headers[HTTP2_HEADER_PATH] = path
- headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
- // https://tools.ietf.org/html/rfc7231#section-4.3.1
- // https://tools.ietf.org/html/rfc7231#section-4.3.2
- // https://tools.ietf.org/html/rfc7231#section-4.3.5
- // Sending a payload body on a request that does not
- // expect it can cause undefined behavior on some
- // servers and corrupt connection state. Do not
- // re-use the connection for further requests.
- const expectsPayload = (
- method === 'PUT' ||
- method === 'POST' ||
- method === 'PATCH'
- )
- if (body && typeof body.read === 'function') {
- // Try to read EOF in order to get length.
- body.read(0)
- }
- let contentLength = util.bodyLength(body)
- if (util.isFormDataLike(body)) {
- extractBody ??= require('../web/fetch/body.js').extractBody
- const [bodyStream, contentType] = extractBody(body)
- headers['content-type'] = contentType
- body = bodyStream.stream
- contentLength = bodyStream.length
- }
- if (contentLength == null) {
- contentLength = request.contentLength
- }
- if (!expectsPayload) {
- // https://tools.ietf.org/html/rfc7230#section-3.3.2
- // A user agent SHOULD NOT send a Content-Length header field when
- // the request message does not contain a payload body and the method
- // semantics do not anticipate such a body.
- // And for methods that don't expect a payload, omit Content-Length.
- contentLength = null
- }
- // https://github.com/nodejs/undici/issues/2046
- // A user agent may send a Content-Length header with 0 value, this should be allowed.
- if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
- if (client[kStrictContentLength]) {
- util.errorRequest(client, request, new RequestContentLengthMismatchError())
- return false
- }
- process.emitWarning(new RequestContentLengthMismatchError())
- }
- if (contentLength != null) {
- assert(body || contentLength === 0, 'no body must not have content length')
- headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
- }
- session.ref()
- if (channels.sendHeaders.hasSubscribers) {
- let header = ''
- for (const key in headers) {
- header += `${key}: ${headers[key]}\r\n`
- }
- channels.sendHeaders.publish({ request, headers: header, socket: session[kSocket] })
- }
- // TODO(metcoder95): add support for sending trailers
- const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
- if (expectContinue) {
- headers[HTTP2_HEADER_EXPECT] = '100-continue'
- stream = session.request(headers, { endStream: shouldEndStream, signal })
- stream[kHTTP2Stream] = true
- stream.once('continue', writeBodyH2)
- } else {
- stream = session.request(headers, {
- endStream: shouldEndStream,
- signal
- })
- stream[kHTTP2Stream] = true
- writeBodyH2()
- }
- // Increment counter as we have new streams open
- ++session[kOpenStreams]
- stream.setTimeout(headersTimeout)
- // Track whether we received a response (headers)
- let responseReceived = false
- stream.once('response', headers => {
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
- request.onResponseStarted()
- responseReceived = true
- stream.setTimeout(bodyTimeout)
- // Due to the stream nature, it is possible we face a race condition
- // where the stream has been assigned, but the request has been aborted
- // the request remains in-flight and headers hasn't been received yet
- // for those scenarios, best effort is to destroy the stream immediately
- // as there's no value to keep it open.
- if (request.aborted) {
- stream.removeAllListeners('data')
- return
- }
- if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
- stream.pause()
- }
- stream.on('data', (chunk) => {
- if (request.aborted || request.completed) {
- return
- }
- if (request.onData(chunk) === false) {
- stream.pause()
- }
- })
- })
- stream.once('end', () => {
- stream.removeAllListeners('data')
- // If we received a response, this is a normal completion
- if (responseReceived) {
- if (!request.aborted && !request.completed) {
- request.onComplete({})
- }
- completeRequest(client, request)
- client[kResume]()
- } else {
- // Stream ended without receiving a response - this is an error
- // (e.g., server destroyed the stream before sending headers)
- abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
- completeRequest(client, request, true)
- client[kResume]()
- }
- })
- stream.once('close', () => {
- stream.removeAllListeners('data')
- session[kOpenStreams] -= 1
- if (session[kOpenStreams] === 0) {
- session.unref()
- }
- // A stream can close without ever emitting 'end' or 'error': a peer's
- // RST_STREAM(CANCEL) received before the response is reported by Node as a
- // bare 'close', and destroying the stream unenrolls its timeout, so no
- // 'timeout' follows either. Nothing else would ever settle this request.
- if (!request.aborted && !request.completed) {
- abort(new InformationalError('HTTP/2: stream closed before the response was complete'))
- }
- })
- stream.once('error', function (err) {
- stream.removeAllListeners('data')
- abort(err)
- })
- stream.once('frameError', (type, code) => {
- stream.removeAllListeners('data')
- abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
- })
- stream.on('aborted', () => {
- stream.removeAllListeners('data')
- })
- stream.on('timeout', () => {
- const err = responseReceived
- ? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
- : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
- stream.removeAllListeners('data')
- session[kOpenStreams] -= 1
- if (session[kOpenStreams] === 0) {
- session.unref()
- }
- abort(err)
- })
- stream.once('trailers', trailers => {
- if (request.aborted || request.completed) {
- return
- }
- stream.removeAllListeners('data')
- request.onComplete(trailers)
- })
- return true
- function writeBodyH2 () {
- if (!body || contentLength === 0) {
- writeBuffer(
- abort,
- stream,
- null,
- client,
- request,
- client[kSocket],
- contentLength,
- expectsPayload
- )
- } else if (util.isBuffer(body)) {
- writeBuffer(
- abort,
- stream,
- body,
- client,
- request,
- client[kSocket],
- contentLength,
- expectsPayload
- )
- } else if (util.isBlobLike(body)) {
- if (typeof body.stream === 'function') {
- writeIterable(
- abort,
- stream,
- body.stream(),
- client,
- request,
- client[kSocket],
- contentLength,
- expectsPayload
- )
- } else {
- writeBlob(
- abort,
- stream,
- body,
- client,
- request,
- client[kSocket],
- contentLength,
- expectsPayload
- )
- }
- } else if (util.isStream(body)) {
- writeStream(
- abort,
- client[kSocket],
- expectsPayload,
- stream,
- body,
- client,
- request,
- contentLength
- )
- } else if (util.isIterable(body)) {
- writeIterable(
- abort,
- stream,
- body,
- client,
- request,
- client[kSocket],
- contentLength,
- expectsPayload
- )
- } else {
- assert(false)
- }
- }
- }
- function writeBuffer (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
- try {
- if (body != null && util.isBuffer(body)) {
- assert(contentLength === body.byteLength, 'buffer body must have content length')
- h2stream.cork()
- h2stream.write(body)
- h2stream.uncork()
- h2stream.end()
- request.onBodySent(body)
- }
- if (!expectsPayload) {
- socket[kReset] = true
- }
- request.onRequestSent()
- client[kResume]()
- } catch (error) {
- abort(error)
- }
- }
- function writeStream (abort, socket, expectsPayload, h2stream, body, client, request, contentLength) {
- assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
- // For HTTP/2, is enough to pipe the stream
- const pipe = pipeline(
- body,
- h2stream,
- (err) => {
- if (err) {
- util.destroy(pipe, err)
- abort(err)
- } else {
- util.removeAllListeners(pipe)
- request.onRequestSent()
- if (!expectsPayload) {
- socket[kReset] = true
- }
- client[kResume]()
- }
- }
- )
- util.addListener(pipe, 'data', onPipeData)
- function onPipeData (chunk) {
- request.onBodySent(chunk)
- }
- }
- async function writeBlob (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
- assert(contentLength === body.size, 'blob body must have content length')
- try {
- if (contentLength != null && contentLength !== body.size) {
- throw new RequestContentLengthMismatchError()
- }
- const buffer = Buffer.from(await body.arrayBuffer())
- h2stream.cork()
- h2stream.write(buffer)
- h2stream.uncork()
- h2stream.end()
- request.onBodySent(buffer)
- request.onRequestSent()
- if (!expectsPayload) {
- socket[kReset] = true
- }
- client[kResume]()
- } catch (err) {
- abort(err)
- }
- }
- async function writeIterable (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
- assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
- let callback = null
- function onDrain () {
- if (callback) {
- const cb = callback
- callback = null
- cb()
- }
- }
- const waitForDrain = () => new Promise((resolve, reject) => {
- assert(callback === null)
- if (socket[kError]) {
- reject(socket[kError])
- } else {
- callback = resolve
- }
- })
- h2stream
- .on('close', onDrain)
- .on('drain', onDrain)
- try {
- // It's up to the user to somehow abort the async iterable.
- for await (const chunk of body) {
- if (socket[kError]) {
- throw socket[kError]
- }
- const res = h2stream.write(chunk)
- request.onBodySent(chunk)
- if (!res) {
- await waitForDrain()
- }
- }
- h2stream.end()
- request.onRequestSent()
- if (!expectsPayload) {
- socket[kReset] = true
- }
- client[kResume]()
- } catch (err) {
- abort(err)
- } finally {
- h2stream
- .off('close', onDrain)
- .off('drain', onDrain)
- }
- }
- module.exports = connectH2
|