diff --git a/docs/docs/api/Interceptors.md b/docs/docs/api/Interceptors.md index c692442d7f0..34bdd2eca8f 100644 --- a/docs/docs/api/Interceptors.md +++ b/docs/docs/api/Interceptors.md @@ -198,7 +198,8 @@ const agent = new Agent().compose( ## `interceptors.decompress([opts])` Automatically decompresses response bodies encoded with `gzip`, `x-gzip`, -`br` (Brotli), `deflate`, `compress`, `x-compress`, or `zstd`. +`br` (Brotli), `deflate`, or `zstd`. Any other coding, including `compress` +and `x-compress`, is left untouched, matching `fetch()`. > **Experimental:** This interceptor is experimental and subject to change. > A one-time `ExperimentalWarning` is emitted on first use. diff --git a/lib/interceptor/decompress.js b/lib/interceptor/decompress.js index c675de4197c..88f48f2199d 100644 --- a/lib/interceptor/decompress.js +++ b/lib/interceptor/decompress.js @@ -8,14 +8,16 @@ const DecoratorHandler = require('../handler/decorator-handler') /** @typedef {import('node:stream').Transform} Controller */ /** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */ +// `compress` and `x-compress` are deliberately absent. RFC 9110 section +// 8.4.1.1 defines them as the UNIX compress (LZW) format, which zlib cannot +// read, so inflating them turns a readable response into a Z_DATA_ERROR. +// fetch() leaves them alone, and so does this interceptor. /** @type {Record DecompressorStream>} */ const supportedEncodings = { gzip: createGunzip, 'x-gzip': createGunzip, br: createBrotliDecompress, deflate: createInflate, - compress: createInflate, - 'x-compress': createInflate, zstd: createZstdDecompress } diff --git a/test/interceptors/decompress.js b/test/interceptors/decompress.js index ea6af870047..05e47a3581f 100644 --- a/test/interceptors/decompress.js +++ b/test/interceptors/decompress.js @@ -1206,3 +1206,51 @@ test('should work with global dispatcher for both fetch() and request()', async await t.completed }) + +test('should pass through a compress-encoded response instead of inflating it', async t => { + t = tspl(t, { plan: 3 }) + + const net = require('node:net') + + // Content-Encoding: compress is the UNIX compress (LZW) format per RFC 9110 + // section 8.4.1.1, not zlib, so this body is not inflatable. + const payload = Buffer.from('this body is not deflate data') + + const server = net.createServer(socket => { + socket.once('data', () => { + socket.write( + 'HTTP/1.1 200 OK\r\n' + + 'Content-Type: text/plain\r\n' + + 'Content-Encoding: compress\r\n' + + `Content-Length: ${payload.length}\r\n` + + 'Connection: close\r\n' + + '\r\n' + ) + socket.end(payload) + }) + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client( + `http://localhost:${server.address().port}` + ).compose(createDecompressInterceptor()) + + after(async () => { + await client.close() + server.close() + await once(server, 'close') + }) + + const response = await client.request({ + method: 'GET', + path: '/' + }) + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-encoding'], 'compress', 'the encoding is left on the response since nothing was decoded') + t.equal(await response.body.text(), payload.toString()) + + await t.completed +})