-
-
Notifications
You must be signed in to change notification settings - Fork 735
Decompression Interceptor #4316
Copy link
Copy link
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Add decompression interceptor for automatic response decompression
On #3253, a future proposal was made for the addition of a decompression interceptor and I think this would be a worthwhile feature.
Benefits
Currently, fetch() automatically handles compression while request() requires manual decompression. This interceptor bridges that gap, making request() behavior consistent with fetch() and reduces boilerplate for handling compressed responses.
Implementation Details
- Supports gzip, deflate, and brotli compression
- Streaming decompression (memory efficient)
- Proper header cleanup (removes content-encoding/length)
- Error handling with proper cleanup
- Comprehensive test coverage
Usage Example
const { Client } = require('undici')
const createDecompressInterceptor = require('./decompress')
const client = new Client('https://api.example.com')
.compose(createDecompressInterceptor())
// Now all responses are automatically decompressed
const response = await client.request({ path: '/data' })Implementation
I've implemented a solution (will link draft PR) with tests and would be interested in getting some feedback. Below is what the interceptor would look like:
'use strict'
const { createInflate, createGunzip, createBrotliDecompress } = require('node:zlib')
const DecoratorHandler = require('../handler/decorator-handler')
class DecompressHandler extends DecoratorHandler {
#decompressor = null
onResponseStart (controller, statusCode, headers, statusMessage) {
const contentEncoding = headers['content-encoding']
const shouldSkip = !contentEncoding || statusCode < 200 || statusCode >= 400 || [204, 304].includes(statusCode)
if (shouldSkip) {
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
// Remove content-encoding header since we're decompressing
const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
const supportedEncodings = {
gzip: createGunzip,
deflate: createInflate,
br: createBrotliDecompress
}
const createDecompressor = supportedEncodings[contentEncoding.toLowerCase()]
if (!createDecompressor) {
return super.onResponseStart(controller, statusCode, headers, statusMessage)
}
this.#decompressor = createDecompressor()
// Set up decompressor event handlers
this.#decompressor.on('data', (chunk) => {
super.onResponseData(controller, chunk)
})
this.#decompressor.on('error', (error) => {
super.onResponseError(controller, error)
})
this.#decompressor.on('finish', () => {
super.onResponseEnd(controller, {})
})
return super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
}
onResponseData (controller, chunk) {
// If we have a decompressor, pipe data through it
if (this.#decompressor) {
this.#decompressor.write(chunk)
return true
}
return super.onResponseData(controller, chunk)
}
onResponseEnd (controller, trailers) {
if (this.#decompressor) {
this.#decompressor.end()
this.#decompressor = null
return
}
return super.onResponseEnd(controller, trailers)
}
onResponseError (controller, err) {
if (this.#decompressor) {
this.#decompressor.destroy(err)
this.#decompressor = null
}
return super.onResponseError(controller, err)
}
}
function createDecompressInterceptor () {
return (dispatch) => {
return (opts, handler) => {
const decompressHandler = new DecompressHandler(handler)
return dispatch(opts, decompressHandler)
}
}
}
module.exports = createDecompressInterceptorI'm happy to finish this up and add documentation if this is something that would be of benefit.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request