Skip to content

Fix duplicate Content-Length header in CompressionHandler.compressBuffer()#60

Merged
VikramAditya33 merged 1 commit into
mainfrom
fix-compression
Apr 28, 2026
Merged

Fix duplicate Content-Length header in CompressionHandler.compressBuffer()#60
VikramAditya33 merged 1 commit into
mainfrom
fix-compression

Conversation

@VikramAditya33

@VikramAditya33 VikramAditya33 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Remove res.setHeader('content-length', ...) from compressBuffer(). Let send() handle it automatically based on the returned buffer size.

Fix #57

Summary by CodeRabbit

  • Bug Fixes
    • Improved response header management during content compression to prevent duplicate headers and ensure accurate content size information is communicated to clients when responses are compressed.

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 70078673-27ad-4f24-a3cf-c98fe0cbdbf2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-compression

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

uWestJS Benchmark Results

Scenario Express req/s Fastify req/s uWestJS req/s Express throughput Fastify throughput uWestJS throughput uWestJS vs Express uWestJS vs Fastify
compression 6.47k 5.05k 7.65k 3.40 MB/s 2.59 MB/s 3.52 MB/s 1.18x 1.51x
headers 21.76k 35.57k 60.24k 4.05 MB/s 6.65 MB/s 9.48 MB/s 2.77x 1.69x
hello-world 22.93k 37.39k 72.89k 3.80 MB/s 6.28 MB/s 7.23 MB/s 3.18x 1.95x
json-response 21.18k 35.68k 59.24k 5.78 MB/s 9.76 MB/s 14.46 MB/s 2.80x 1.66x
mixed-response 21.92k 35.13k 65.31k 5.06 MB/s 8.14 MB/s 13.20 MB/s 2.98x 1.86x
post-json 19.95k 16.62k 33.52k 3.69 MB/s 4.53 MB/s 5.69 MB/s 1.68x 2.02x
query-params 17.31k 29.55k 65.14k 3.81 MB/s 6.54 MB/s 8.76 MB/s 3.76x 2.20x
route-params 21.00k 35.00k 63.72k 4.79 MB/s 8.01 MB/s 12.70 MB/s 3.03x 1.82x
static-file 22.85k 34.91k 57.21k 226.78 MB/s 346.04 MB/s 566.19 MB/s 2.50x 1.64x

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1f2bad7 and d925a59.

📒 Files selected for processing (2)
  • src/http/handlers/compression/compression-handler.spec.ts
  • src/http/handlers/compression/compression-handler.ts

Comment on lines +820 to 838
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);
});

@coderabbitai coderabbitai Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VikramAditya33, you're absolutely right, and I apologize for the incorrect suggestion. The design is intentionally correct:

  1. Avoiding duplicate headers — if compressBuffer() sets Content-Length and send() (or uWS end()) also sets it, the result is the HPE_UNEXPECTED_CONTENT_LENGTH error that this very PR is fixing.
  2. 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.
  3. uWS auto-sets Content-Length — calling uwsRes.end(buffer) automatically derives and sets Content-Length from 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.

Comment on lines 334 to 338
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);

@coderabbitai coderabbitai Bot Apr 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 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:

  1. createCompressionStream() already calls res.removeHeader('content-length') to strip any pre-existing length header before compression.
  2. compressBuffer() (after this PR) no longer calls res.setHeader('content-length', ...).
  3. uwsRes.end(buffer) — the native uWS layer — automatically injects a Content-Length header 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.

@VikramAditya33 VikramAditya33 merged commit 3b0a0f7 into main Apr 28, 2026
4 checks passed
@VikramAditya33 VikramAditya33 deleted the fix-compression branch April 28, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate Content-Length Bug

1 participant