diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index d7bb32e47c1..a0dd75df7a5 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -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' @@ -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({ @@ -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 diff --git a/package.json b/package.json index 1140c62190b..af3172a1c8e 100644 --- a/package.json +++ b/package.json @@ -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", "test:javascript:without-intl": "npm run test:javascript:no-jest", "test:busboy": "borp -p \"test/busboy/*.js\"", "test:cache": "borp -p \"test/cache/*.js\"", diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index 00e8732fd9c..077b4a401ce 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -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' + ) { + 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 + }) +})