Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ const { webidl } = require('../webidl')
const { STATUS_CODES } = require('node:http')
const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
const { createDeferredPromise } = require('../../util/promise')

const hasZstd = typeof zlib.createZstdDecompress === 'function'

const GET_OR_HEAD = ['GET', 'HEAD']

const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
Expand Down Expand Up @@ -2104,33 +2107,29 @@ async function httpNetworkFetch (
return false
}

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

const headersList = new HeadersList()

for (let i = 0; i < rawHeaders.length; i += 2) {
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
}
const contentEncoding = headersList.get('content-encoding', true)
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())
}
const location = headersList.get('location', true)

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

const decoders = []

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

const decoders = []

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
const contentEncoding = headersList.get('content-encoding', true)
// "All content-coding values are case-insensitive..."
/** @type {string[]} */
const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
for (let i = codings.length - 1; i >= 0; --i) {
const coding = codings[i]
const coding = codings[i].trim()
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
if (coding === 'x-gzip' || coding === 'gzip') {
decoders.push(zlib.createGunzip({
Expand All @@ -2151,8 +2150,8 @@ async function httpNetworkFetch (
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}))
} else if (coding === 'zstd' && typeof zlib.createZstdDecompress === 'function') {
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
} else if (coding === 'zstd' && hasZstd) {
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
decoders.push(zlib.createZstdDecompress({
flush: zlib.constants.ZSTD_e_continue,
finishFlush: zlib.constants.ZSTD_e_end
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"lint:fix": "eslint --fix --cache",
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
"test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:subresource-integrity && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests",
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:fetch && npm run test:node-fetch && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:cookies && npm run test:eventsource && npm run test:subresource-integrity && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just moving test:fetch earlier in the order? Does that really matter? i hope the tests are independant

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are, but it makes more sense to test the fetch stuff consequently.

"test:javascript:without-intl": "npm run test:javascript:no-jest",
"test:busboy": "borp -p \"test/busboy/*.js\"",
"test:cache": "borp -p \"test/cache/*.js\"",
Expand Down
146 changes: 78 additions & 68 deletions test/fetch/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,92 +2,102 @@

const { once } = require('node:events')
const { createServer } = require('node:http')
const { test } = require('node:test')
const { test, before, after, describe } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')
const { fetch } = require('../..')

test('content-encoding header', async (t) => {
const { strictEqual } = tspl(t, { plan: 2 })

const contentEncoding = 'deflate, gzip'
const text = 'Hello, World!'
describe('content-encoding handling', () => {
const gzipDeflateText = Buffer.from('H4sIAAAAAAAAA6uY89nj7MmT1wM5zuuf8gxkYZCfx5IFACQ8u/wVAAAA', 'base64')

const server = createServer((req, res) => {
res.writeHead(200,
{
'Content-Encoding': contentEncoding,
'Content-Type': 'text/plain'
const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64')

let server
before(async () => {
server = createServer({
noDelay: true
}, (req, res) => {
res.socket.setNoDelay(true)
if (
req.headers['accept-encoding'] === 'deflate, gzip' ||
req.headers['accept-encoding'] === 'DeFlAtE, GzIp'
Comment on lines +20 to +21
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
req.headers['accept-encoding'] === 'deflate, gzip' ||
req.headers['accept-encoding'] === 'DeFlAtE, GzIp'
req.headers['accept-encoding']?.toLowerCase() === 'deflate, gzip'

) {
res.writeHead(200,
{
'Content-Encoding': 'deflate, gzip',
'Content-Type': 'text/plain'
}
)
res.flushHeaders()
res.end(gzipDeflateText)
} else if (req.headers['accept-encoding'] === 'zstd') {
res.writeHead(200,
{
'Content-Encoding': 'zstd',
'Content-Type': 'text/plain'
}
)
res.flushHeaders()
res.end(zstdText)
} else {
res.writeHead(200,
{
'Content-Type': 'text/plain'
}
)
res.flushHeaders()
res.end('Hello, World!')
}
)
.end(gzipDeflateText)
})
await once(server.listen(0), 'listening')
})
await once(server.listen(0), 'listening')

const response = await fetch(`http://localhost:${server.address().port}`)

strictEqual(response.headers.get('content-encoding'), contentEncoding)
strictEqual(await response.text(), text)

await t.completed
server.close()
})

test('content-encoding header is case-iNsENsITIve', async (t) => {
const { strictEqual } = tspl(t, { plan: 2 })

const contentEncoding = 'DeFlAtE, GzIp'
const text = 'Hello, World!'
const gzipDeflateText = Buffer.from('H4sIAAAAAAAAA6uY89nj7MmT1wM5zuuf8gxkYZCfx5IFACQ8u/wVAAAA', 'base64')

const server = createServer((req, res) => {
res.writeHead(200,
{
'Content-Encoding': contentEncoding,
'Content-Type': 'text/plain'
}
)
.end(gzipDeflateText)
after(() => {
server.close()
})

await once(server.listen(0), 'listening')
test('content-encoding header', async (t) => {
const { strictEqual } = tspl(t, { plan: 3 })

const response = await fetch(`http://localhost:${server.address().port}`)
const response = await fetch(`http://localhost:${server.address().port}`, {
keepalive: false,
headers: { 'accept-encoding': 'deflate, gzip' }
})

strictEqual(response.headers.get('content-encoding'), contentEncoding)
strictEqual(await response.text(), text)
strictEqual(response.headers.get('content-encoding'), 'deflate, gzip')
strictEqual(response.headers.get('content-type'), 'text/plain')
strictEqual(await response.text(), 'Hello, World!')

await t.completed
server.close()
})
await t.completed
})

test('should decompress zstandard response',
{ skip: typeof require('node:zlib').createZstdDecompress !== 'function' },
async (t) => {
test('content-encoding header is case-iNsENsITIve', async (t) => {
const { strictEqual } = tspl(t, { plan: 3 })

const contentEncoding = 'zstd'
const text = 'Hello, World!'
const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64')

const server = createServer((req, res) => {
res.writeHead(200,
{
'Content-Encoding': contentEncoding,
'Content-Type': 'text/plain'
})
.end(zstdText)
const response = await fetch(`http://localhost:${server.address().port}`, {
keepalive: false,
headers: { 'accept-encoding': 'DeFlAtE, GzIp' }
})

await once(server.listen(0), 'listening')

const url = `http://localhost:${server.address().port}`

const response = await fetch(url)
strictEqual(await response.text(), text)
strictEqual(response.headers.get('content-encoding'), contentEncoding)
strictEqual(response.headers.get('content-encoding'), 'deflate, gzip')
strictEqual(response.headers.get('content-type'), 'text/plain')
strictEqual(await response.text(), 'Hello, World!')

await t.completed
server.close()
})

test('should decompress zstandard response',
{ skip: typeof require('node:zlib').createZstdDecompress !== 'function' },
async (t) => {
const { strictEqual } = tspl(t, { plan: 3 })

const response = await fetch(`http://localhost:${server.address().port}`, {
keepalive: false,
headers: { 'accept-encoding': 'zstd' }
})

strictEqual(response.headers.get('content-encoding'), 'zstd')
strictEqual(response.headers.get('content-type'), 'text/plain')
strictEqual(await response.text(), 'Hello, World!')

await t.completed
})
})
Loading