Skip to content

Commit d7bb09e

Browse files
authored
fetch: process content-encoding header only if relevant (#4496)
* perf: process content-encoding header only if relevant * patch test * change order of tests and run fetch tests before the cache tests * disable keepAlive * set keepalive to false * what now?
1 parent e652f03 commit d7bb09e

File tree

3 files changed

+93
-84
lines changed

3 files changed

+93
-84
lines changed

lib/web/fetch/index.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ const { webidl } = require('../webidl')
6363
const { STATUS_CODES } = require('node:http')
6464
const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
6565
const { createDeferredPromise } = require('../../util/promise')
66+
67+
const hasZstd = typeof zlib.createZstdDecompress === 'function'
68+
6669
const GET_OR_HEAD = ['GET', 'HEAD']
6770

6871
const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
@@ -2104,33 +2107,29 @@ async function httpNetworkFetch (
21042107
return false
21052108
}
21062109

2107-
/** @type {string[]} */
2108-
let codings = []
2109-
21102110
const headersList = new HeadersList()
21112111

21122112
for (let i = 0; i < rawHeaders.length; i += 2) {
21132113
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
21142114
}
2115-
const contentEncoding = headersList.get('content-encoding', true)
2116-
if (contentEncoding) {
2117-
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2118-
// "All content-coding values are case-insensitive..."
2119-
codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim())
2120-
}
21212115
const location = headersList.get('location', true)
21222116

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

2125-
const decoders = []
2126-
21272119
const willFollow = location && request.redirect === 'follow' &&
21282120
redirectStatusSet.has(status)
21292121

2122+
const decoders = []
2123+
21302124
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
2131-
if (codings.length !== 0 && request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2125+
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2126+
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
2127+
const contentEncoding = headersList.get('content-encoding', true)
2128+
// "All content-coding values are case-insensitive..."
2129+
/** @type {string[]} */
2130+
const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
21322131
for (let i = codings.length - 1; i >= 0; --i) {
2133-
const coding = codings[i]
2132+
const coding = codings[i].trim()
21342133
// https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
21352134
if (coding === 'x-gzip' || coding === 'gzip') {
21362135
decoders.push(zlib.createGunzip({
@@ -2151,8 +2150,8 @@ async function httpNetworkFetch (
21512150
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
21522151
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
21532152
}))
2154-
} else if (coding === 'zstd' && typeof zlib.createZstdDecompress === 'function') {
2155-
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
2153+
} else if (coding === 'zstd' && hasZstd) {
2154+
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
21562155
decoders.push(zlib.createZstdDecompress({
21572156
flush: zlib.constants.ZSTD_e_continue,
21582157
finishFlush: zlib.constants.ZSTD_e_end

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"lint:fix": "eslint --fix --cache",
7070
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript",
7171
"test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
72-
"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",
72+
"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",
7373
"test:javascript:without-intl": "npm run test:javascript:no-jest",
7474
"test:busboy": "borp -p \"test/busboy/*.js\"",
7575
"test:cache": "borp -p \"test/cache/*.js\"",

test/fetch/encoding.js

Lines changed: 78 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,92 +2,102 @@
22

33
const { once } = require('node:events')
44
const { createServer } = require('node:http')
5-
const { test } = require('node:test')
5+
const { test, before, after, describe } = require('node:test')
66
const { tspl } = require('@matteo.collina/tspl')
77
const { fetch } = require('../..')
88

9-
test('content-encoding header', async (t) => {
10-
const { strictEqual } = tspl(t, { plan: 2 })
11-
12-
const contentEncoding = 'deflate, gzip'
13-
const text = 'Hello, World!'
9+
describe('content-encoding handling', () => {
1410
const gzipDeflateText = Buffer.from('H4sIAAAAAAAAA6uY89nj7MmT1wM5zuuf8gxkYZCfx5IFACQ8u/wVAAAA', 'base64')
15-
16-
const server = createServer((req, res) => {
17-
res.writeHead(200,
18-
{
19-
'Content-Encoding': contentEncoding,
20-
'Content-Type': 'text/plain'
11+
const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64')
12+
13+
let server
14+
before(async () => {
15+
server = createServer({
16+
noDelay: true
17+
}, (req, res) => {
18+
res.socket.setNoDelay(true)
19+
if (
20+
req.headers['accept-encoding'] === 'deflate, gzip' ||
21+
req.headers['accept-encoding'] === 'DeFlAtE, GzIp'
22+
) {
23+
res.writeHead(200,
24+
{
25+
'Content-Encoding': 'deflate, gzip',
26+
'Content-Type': 'text/plain'
27+
}
28+
)
29+
res.flushHeaders()
30+
res.end(gzipDeflateText)
31+
} else if (req.headers['accept-encoding'] === 'zstd') {
32+
res.writeHead(200,
33+
{
34+
'Content-Encoding': 'zstd',
35+
'Content-Type': 'text/plain'
36+
}
37+
)
38+
res.flushHeaders()
39+
res.end(zstdText)
40+
} else {
41+
res.writeHead(200,
42+
{
43+
'Content-Type': 'text/plain'
44+
}
45+
)
46+
res.flushHeaders()
47+
res.end('Hello, World!')
2148
}
22-
)
23-
.end(gzipDeflateText)
49+
})
50+
await once(server.listen(0), 'listening')
2451
})
25-
await once(server.listen(0), 'listening')
26-
27-
const response = await fetch(`http://localhost:${server.address().port}`)
28-
29-
strictEqual(response.headers.get('content-encoding'), contentEncoding)
30-
strictEqual(await response.text(), text)
31-
32-
await t.completed
33-
server.close()
34-
})
3552

36-
test('content-encoding header is case-iNsENsITIve', async (t) => {
37-
const { strictEqual } = tspl(t, { plan: 2 })
38-
39-
const contentEncoding = 'DeFlAtE, GzIp'
40-
const text = 'Hello, World!'
41-
const gzipDeflateText = Buffer.from('H4sIAAAAAAAAA6uY89nj7MmT1wM5zuuf8gxkYZCfx5IFACQ8u/wVAAAA', 'base64')
42-
43-
const server = createServer((req, res) => {
44-
res.writeHead(200,
45-
{
46-
'Content-Encoding': contentEncoding,
47-
'Content-Type': 'text/plain'
48-
}
49-
)
50-
.end(gzipDeflateText)
53+
after(() => {
54+
server.close()
5155
})
5256

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

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

57-
strictEqual(response.headers.get('content-encoding'), contentEncoding)
58-
strictEqual(await response.text(), text)
65+
strictEqual(response.headers.get('content-encoding'), 'deflate, gzip')
66+
strictEqual(response.headers.get('content-type'), 'text/plain')
67+
strictEqual(await response.text(), 'Hello, World!')
5968

60-
await t.completed
61-
server.close()
62-
})
69+
await t.completed
70+
})
6371

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

69-
const contentEncoding = 'zstd'
70-
const text = 'Hello, World!'
71-
const zstdText = Buffer.from('KLUv/QBYaQAASGVsbG8sIFdvcmxkIQ==', 'base64')
72-
73-
const server = createServer((req, res) => {
74-
res.writeHead(200,
75-
{
76-
'Content-Encoding': contentEncoding,
77-
'Content-Type': 'text/plain'
78-
})
79-
.end(zstdText)
75+
const response = await fetch(`http://localhost:${server.address().port}`, {
76+
keepalive: false,
77+
headers: { 'accept-encoding': 'DeFlAtE, GzIp' }
8078
})
8179

82-
await once(server.listen(0), 'listening')
83-
84-
const url = `http://localhost:${server.address().port}`
85-
86-
const response = await fetch(url)
87-
strictEqual(await response.text(), text)
88-
strictEqual(response.headers.get('content-encoding'), contentEncoding)
80+
strictEqual(response.headers.get('content-encoding'), 'deflate, gzip')
8981
strictEqual(response.headers.get('content-type'), 'text/plain')
82+
strictEqual(await response.text(), 'Hello, World!')
9083

9184
await t.completed
92-
server.close()
9385
})
86+
87+
test('should decompress zstandard response',
88+
{ skip: typeof require('node:zlib').createZstdDecompress !== 'function' },
89+
async (t) => {
90+
const { strictEqual } = tspl(t, { plan: 3 })
91+
92+
const response = await fetch(`http://localhost:${server.address().port}`, {
93+
keepalive: false,
94+
headers: { 'accept-encoding': 'zstd' }
95+
})
96+
97+
strictEqual(response.headers.get('content-encoding'), 'zstd')
98+
strictEqual(response.headers.get('content-type'), 'text/plain')
99+
strictEqual(await response.text(), 'Hello, World!')
100+
101+
await t.completed
102+
})
103+
})

0 commit comments

Comments
 (0)