Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(fetch): optimize call dispatch #2493

Merged
merged 15 commits into from
Dec 8, 2023
81 changes: 38 additions & 43 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const {
filterResponse,
makeResponse
} = require('./response')
const { Headers } = require('./headers')
const { Headers, HeadersList } = require('./headers')
const { Request, makeRequest } = require('./request')
const zlib = require('zlib')
const {
Expand Down Expand Up @@ -2075,7 +2075,7 @@ async function httpNetworkFetch (
// 20. Return response.
return response

async function dispatch ({ body }) {
function dispatch ({ body }) {
const url = requestCurrentURL(request)
/** @type {import('../..').Agent} */
const agent = fetchParams.controller.dispatcher
Expand All @@ -2085,7 +2085,7 @@ async function httpNetworkFetch (
path: url.pathname + url.search,
origin: url.origin,
method: request.method,
body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
headers: request.headersList.entries,
maxRedirections: 0,
upgrade: request.mode === 'websocket' ? 'websocket' : undefined
Expand All @@ -2106,59 +2106,57 @@ async function httpNetworkFetch (
}
},

onHeaders (status, headersList, resume, statusText) {
onHeaders (status, rawHeaders, resume, statusText) {
if (status < 200) {
return
}

/** @type {string[]} */
let codings = []
let location = ''

const headers = new Headers()
const headersList = new HeadersList()

// For H2, the headers are a plain JS object
// For H2, the rawHeaders are a plain JS object
// We distinguish between them and iterate accordingly
if (Array.isArray(headersList)) {
for (let n = 0; n < headersList.length; n += 2) {
const key = headersList[n + 0].toString('latin1')
const val = headersList[n + 1].toString('latin1')
if (key.toLowerCase() === 'content-encoding') {
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
// "All content-coding values are case-insensitive..."
codings = val.toLowerCase().split(',').map((x) => x.trim())
} else if (key.toLowerCase() === 'location') {
location = val
}

headers[kHeadersList].append(key, val)
if (Array.isArray(rawHeaders)) {
for (let i = 0; i < rawHeaders.length; i += 2) {
headersList.append(rawHeaders[i].toString('latin1'), rawHeaders[i + 1].toString('latin1'))
tsctx marked this conversation as resolved.
Show resolved Hide resolved
}
const contentEncoding = headersList.get('content-encoding')
if (contentEncoding) {
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
// "All content-coding values are case-insensitive..."
codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
}
location = headersList.get('location')
tsctx marked this conversation as resolved.
Show resolved Hide resolved
} else {
const keys = Object.keys(headersList)
for (const key of keys) {
const val = headersList[key]
if (key.toLowerCase() === 'content-encoding') {
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
// "All content-coding values are case-insensitive..."
codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse()
} else if (key.toLowerCase() === 'location') {
location = val
}

headers[kHeadersList].append(key, val)
const keys = Object.keys(rawHeaders)
for (let i = 0; i < keys.length; ++i) {
headersList.append(keys[i], rawHeaders[keys[i]])
}
// For H2, The header names are already in lowercase,
// so we can avoid the `HeadersList#get` call here.
const contentEncoding = rawHeaders['content-encoding']
if (contentEncoding) {
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
// "All content-coding values are case-insensitive..."
codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()).reverse()
}
location = rawHeaders.location
}

this.body = new Readable({ read: resume })

const decoders = []

const willFollow = request.redirect === 'follow' &&
location &&
const willFollow = location && request.redirect === 'follow' &&
redirectStatusSet.has(status)

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
for (const coding of codings) {
for (let i = 0; i < codings.length; ++i) {
const coding = codings[i]
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
if (coding === 'x-gzip' || coding === 'gzip') {
decoders.push(zlib.createGunzip({
Expand All @@ -2183,7 +2181,7 @@ async function httpNetworkFetch (
resolve({
status,
statusText,
headersList: headers[kHeadersList],
headersList,
body: decoders.length
? pipeline(this.body, ...decoders, () => { })
: this.body.on('error', () => {})
Expand Down Expand Up @@ -2237,24 +2235,21 @@ async function httpNetworkFetch (
reject(error)
},

onUpgrade (status, headersList, socket) {
onUpgrade (status, rawHeaders, socket) {
if (status !== 101) {
return
}

const headers = new Headers()

for (let n = 0; n < headersList.length; n += 2) {
const key = headersList[n + 0].toString('latin1')
const val = headersList[n + 1].toString('latin1')
const headersList = new HeadersList()

headers[kHeadersList].append(key, val)
for (let i = 0; i < rawHeaders.length; i += 2) {
headersList.append(rawHeaders[i].toString('latin1'), rawHeaders[i + 1].toString('latin1'))
}

resolve({
status,
statusText: STATUS_CODES[status],
headersList: headers[kHeadersList],
headersList,
socket
})

Expand Down