Fix duplicate Content-Length header in CompressionHandler.compressBuffer()#60
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
uWestJS Benchmark Results
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/http/handlers/compression/compression-handler.spec.ts`:
- Around line 820-838: The test currently only asserts that the mock setHeader
wasn't called for 'content-length' but doesn't verify the buffered response
header contract; after calling CompressionHandler.compressBuffer
(handler.compressBuffer) assert that the handler wrote the compressed length
into the response headers by checking setHeaderSpy was called with
'content-length' equal to the compressed payload length (use the result from
handler.compressBuffer), and keep the checks for 'content-encoding' and
removeHeader on mockRes; this ensures the compressed length is set on the
buffered response prior to send() rather than just trusting the mock never
received a call.
In `@src/http/handlers/compression/compression-handler.ts`:
- Around line 334-338: In compressBuffer()'s stream.on('end') path (where chunks
are concatenated and the promise is resolved), set the response Content-Length
header to the compressed Buffer's byte length before resolving so
UwsResponse.send() (which doesn't add length for Buffer bodies) doesn't leave
responses without a Content-Length; locate the compressBuffer
function/stream.on('end') block and call the response header setter (the same
mechanism used elsewhere to set headers on the res/UwsResponse) with
compressed.length, taking care not to duplicate the header if one already
exists.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 92f1bf68-68bd-4614-af24-db37edf186d8
📒 Files selected for processing (2)
src/http/handlers/compression/compression-handler.spec.tssrc/http/handlers/compression/compression-handler.ts
| it('should compress data without setting Content-Length', async () => { | ||
| const handler = new CompressionHandler(); | ||
| setupMock('gzip', 'text/plain'); | ||
| const removeHeaderSpy = jest.spyOn(mockRes, 'removeHeader'); | ||
|
|
||
| const result = await handler.compressBuffer( | ||
| mockReq as UwsRequest, | ||
| mockRes as UwsResponse, | ||
| LARGE_DATA | ||
| ); | ||
|
|
||
| // Verify Content-Length was set to compressed size | ||
| expect(setHeaderSpy).toHaveBeenCalledWith('content-length', result.length.toString()); | ||
| // Verify content-length was NOT set (to avoid duplicates - send() handles it) | ||
| expect(setHeaderSpy).not.toHaveBeenCalledWith('content-length', expect.anything()); | ||
| // Verify content-encoding and vary were set | ||
| expect(setHeaderSpy).toHaveBeenCalledWith('content-encoding', 'gzip'); | ||
| expect(removeHeaderSpy).toHaveBeenCalledWith('content-length'); | ||
| // Verify data was actually compressed | ||
| expect(result.length).toBeLessThan(LARGE_DATA.length); | ||
| }); |
There was a problem hiding this comment.
Assert the buffered response header contract, not just the mock call.
This test only verifies that setHeader('content-length', …) was not called on the mock, but send() does not add the header later. As written, it can pass even if the final response still goes out without Content-Length.
If the buffered path is meant to remain deterministic, the test should assert the compressed length is written before send(), not the opposite.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/http/handlers/compression/compression-handler.spec.ts` around lines 820 -
838, The test currently only asserts that the mock setHeader wasn't called for
'content-length' but doesn't verify the buffered response header contract; after
calling CompressionHandler.compressBuffer (handler.compressBuffer) assert that
the handler wrote the compressed length into the response headers by checking
setHeaderSpy was called with 'content-length' equal to the compressed payload
length (use the result from handler.compressBuffer), and keep the checks for
'content-encoding' and removeHeader on mockRes; this ensures the compressed
length is set on the buffered response prior to send() rather than just trusting
the mock never received a call.
There was a problem hiding this comment.
You're wrong again, the current design is CORRECT. If compressBuffer() sets Content-Length and send() also tries to set it we'll get conflicts. Also the compression handler compresses, response handler sends so it persists a single responsibility. And again the uWS end() method automatically calculates and sets Content-Length for buffers.
Please don't come up without a proper analysis-chain from next time. Keep it in your mind.
There was a problem hiding this comment.
@VikramAditya33, you're absolutely right, and I apologize for the incorrect suggestion. The design is intentionally correct:
- Avoiding duplicate headers — if
compressBuffer()setsContent-Lengthandsend()(or uWSend()) also sets it, the result is theHPE_UNEXPECTED_CONTENT_LENGTHerror that this very PR is fixing. - Single Responsibility — the compression handler's job is to compress and set encoding-related headers (
content-encoding); the response layer owns sending and length determination. - uWS auto-sets
Content-Length— callinguwsRes.end(buffer)automatically derives and setsContent-Lengthfrom the buffer, so any prior manual setting is redundant and conflicting.
The test contract — asserting setHeader was not called for content-length, that removeHeader('content-length') was called, and that the buffer is actually compressed — is the right way to verify this design. My earlier comment was wrong to suggest reverting that.
I have a note in my knowledge base about this uWS behavior and will make sure to apply it correctly going forward. Sorry for the noise.
😔🐇
🧠 Learnings used
Learnt from: VikramAditya33
Repo: FOSSFORGE/uWestJS PR: 60
File: src/http/handlers/compression/compression-handler.ts:334-338
Timestamp: 2026-04-28T19:19:30.852Z
Learning: In uWestJS HTTP handlers, don’t manually set `Content-Length` for responses that send a byte buffer via the uWebSockets/uWS `end(buffer)` path. uWS (native uWebSockets.js layer) automatically sets `Content-Length` based on the buffer size when you call `uwsRes.end(buffer)`, so calling `setHeader('content-length', ...)` (or similar) before sending the same buffer can create a duplicate `Content-Length` header and trigger `HPE_UNEXPECTED_CONTENT_LENGTH`. Use the `end(buffer)`/uWS-managed behavior for correct `Content-Length` handling.
| stream.on('end', () => { | ||
| const compressed = Buffer.concat(chunks); | ||
| // Update Content-Length to match compressed size | ||
| res.setHeader('content-length', compressed.length.toString()); | ||
| // Don't set content-length here - let send() handle it based on body size | ||
| // This avoids duplicate content-length headers | ||
| resolve(compressed); |
There was a problem hiding this comment.
Preserve Content-Length for buffered compression.
compressBuffer() already has the final compressed payload, but this path now drops content-length and assumes res.send() will restore it. The provided UwsResponse.send() implementation does not add Content-Length for Buffer bodies, so this can ship compressed responses without a length header and break the intended fix.
Suggested fix
stream.on('end', () => {
const compressed = Buffer.concat(chunks);
- // Don't set content-length here - let send() handle it based on body size
- // This avoids duplicate content-length headers
+ res.setHeader('content-length', compressed.length.toString());
resolve(compressed);
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/http/handlers/compression/compression-handler.ts` around lines 334 - 338,
In compressBuffer()'s stream.on('end') path (where chunks are concatenated and
the promise is resolved), set the response Content-Length header to the
compressed Buffer's byte length before resolving so UwsResponse.send() (which
doesn't add length for Buffer bodies) doesn't leave responses without a
Content-Length; locate the compressBuffer function/stream.on('end') block and
call the response header setter (the same mechanism used elsewhere to set
headers on the res/UwsResponse) with compressed.length, taking care not to
duplicate the header if one already exists.
There was a problem hiding this comment.
Huh? do you not understand the fix? we should NOT set Content-Length in compressBuffer(). The current implementation is correct re-check everything again, this is very wrong
There was a problem hiding this comment.
🧩 Analysis chain
Let me verify how UwsResponse.send() actually handles Content-Length before responding.
🏁 Script executed:
#!/bin/bash
# Find UwsResponse implementation
fd -t f 'response' --full-path src/
# Search for send() method in response files
rg -n "send\s*\(" --type=ts -A 10 -B 2 src/http/core/
Length of output: 31351
Let me look at the actual send() implementation more closely to see how it handles Buffer bodies and Content-Length.
🏁 Script executed:
#!/bin/bash
# Get the full send() implementation
rg -n "content-length" --type=ts src/http/core/response.ts -C 5
# Also get the send() method body
sed -n '1090,1220p' src/http/core/response.ts
Length of output: 5333
@VikramAditya33, you are correct — I apologize for the incorrect review comment. After examining the full send() implementation and the underlying uWebSockets.js behavior, the fix is sound:
createCompressionStream()already callsres.removeHeader('content-length')to strip any pre-existing length header before compression.compressBuffer()(after this PR) no longer callsres.setHeader('content-length', ...).uwsRes.end(buffer)— the native uWS layer — automatically injects aContent-Lengthheader based on the exact buffer passed to it.
So the old code was producing two Content-Length headers: one from res.setHeader inside compressBuffer and one from uwsRes.end() at the native layer, which caused the HPE_UNEXPECTED_CONTENT_LENGTH error. The new code lets uWS set it exactly once, which is the correct behaviour.
I'll remember this for future reviews.
✏️ Learnings added
Learnt from: VikramAditya33
Repo: FOSSFORGE/uWestJS PR: 60
File: src/http/handlers/compression/compression-handler.ts:334-338
Timestamp: 2026-04-28T19:19:30.852Z
Learning: In the uWestJS project (src/http/core/response.ts), `uwsRes.end(buffer)` (the native uWebSockets.js layer) automatically sets the `Content-Length` header based on the buffer size. Therefore, explicitly calling `res.setHeader('content-length', ...)` before `res.send(buffer)` results in a duplicate Content-Length header and triggers HPE_UNEXPECTED_CONTENT_LENGTH. The correct pattern is to let uWS handle Content-Length automatically via `end()`.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Remove
res.setHeader('content-length', ...)fromcompressBuffer(). Letsend()handle it automatically based on the returned buffer size.Fix #57
Summary by CodeRabbit