Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: native HTTP response compression#121

Merged
VikramAditya33 merged 1 commit into
mainfrom
fix/compression
May 11, 2026
Merged

feat: native HTTP response compression#121
VikramAditya33 merged 1 commit into
mainfrom
fix/compression

Conversation

@VikramAditya33
Copy link
Copy Markdown
Collaborator

@VikramAditya33 VikramAditya33 commented May 11, 2026

Wire CompressionHandler into UwsResponse so send() and stream() automatically compress when the client accepts gzip/deflate/br and the response is above threshold.

Scope

  • send(): async compress-then-write inside atomic()
  • stream(): readable.pipe(compressTransform).pipe(this) when applicable
  • Static files, JSON, text all covered automatically

Config

new UwsPlatformAdapter({
  compression: { threshold: 1024, filter?: (req, res) => boolean }
})

Resolves #119

Summary by CodeRabbit

  • New Features

    • Added automatic response compression supporting gzip, deflate, and brotli algorithms
    • Compression now handles both buffered responses and streamed content
    • Compression adapts based on client Accept-Encoding header negotiation
  • Refactor

    • Improved Node stream type compatibility for enhanced stability

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

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: b9e29435-1b5a-4fad-9cda-6a4e455529f6

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:

  • 🔍 Trigger review
✨ 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

github-actions Bot commented May 11, 2026

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.45k 5.32k 8.06k 3.39 MB/s 2.72 MB/s 3.71 MB/s 1.25x 1.52x
headers 21.59k 33.09k 59.66k 4.02 MB/s 6.19 MB/s 9.39 MB/s 2.76x 1.80x
hello-world 21.92k 37.89k 76.02k 3.64 MB/s 6.36 MB/s 7.54 MB/s 3.47x 2.01x
json-response 20.62k 32.91k 64.70k 5.63 MB/s 9.01 MB/s 15.80 MB/s 3.14x 1.97x
mixed-response 22.18k 33.41k 56.96k 5.12 MB/s 7.74 MB/s 11.52 MB/s 2.57x 1.71x
post-json 19.61k 16.50k 36.01k 3.63 MB/s 4.50 MB/s 6.11 MB/s 1.84x 2.18x
query-params 18.74k 34.08k 67.27k 4.13 MB/s 7.54 MB/s 9.05 MB/s 3.59x 1.97x
route-params 20.72k 35.55k 66.33k 4.72 MB/s 8.14 MB/s 13.22 MB/s 3.20x 1.87x
static-file 23.77k 32.36k 54.76k 235.96 MB/s 320.74 MB/s 541.97 MB/s 2.30x 1.69x
streaming-upload 208.69 197.75 228.83 46.47 KB/s 41.33 KB/s 44.25 KB/s 1.10x 1.16x
streaming-with-content-length 416.36 414.71 416.68 2.03 GB/s 2.03 GB/s 2.03 GB/s 1.00x 1.00x
streaming-without-content-length 411.01 408.26 406.79 2.01 GB/s 1.99 GB/s 1.99 GB/s 0.99x 1.00x

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/http/core/response.ts (2)

852-878: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

stream() flushes pending chunks before deciding to compress — produces a mixed-encoding response.

this.flushChunks() runs unconditionally inside the leading atomic(...) (Lines 858-864), which writes any bytes previously buffered by write() calls directly to uWS (and sends headers on the first chunk via writeHead()). Only after that do we evaluate createCompressionStream(...). If a caller did res.write(...); res.stream(readable) with compression enabled, the early bytes go on the wire as plaintext, the compression stream then takes over for the rest, and the client receives a body it cannot decode (and content-encoding either can no longer be set or is set on a partially-plaintext response).

Either decide on compression before flushing (and route any pre-buffered chunks through the compression stream), or document/enforce that write() is not supported before a compressed stream().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/http/core/response.ts` around lines 852 - 878, The stream() method
currently flushes buffered write() chunks via flushChunks() before determining
compressibility, causing mixed-encoding responses; change the order so you call
compressionHandler.createCompressionStream(this.req, this) before invoking
atomic()/flushChunks(), and if a compression stream is returned route any
buffered chunks (from flushChunks()/internal buffer) through the compression
stream and into _streamCompressed(readable, compressStream) rather than writing
them directly to uWS; if no compression stream is returned keep the existing
atomic()/flushChunks() path and call _streamFromReadable(readable, totalSize).

1224-1272: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

send() compression error path can leak corrupt headers and crash the fallback.

Three concrete problems in the compression branch:

  1. The .catch(() => { ... }) swallows the original error with no logging, which makes diagnosing real-world compression failures (corrupt stream, brotli not available, etc.) very hard. At minimum log via the configured logger.
  2. By the time compressBuffer() rejects, compressionHandler may already have mutated the response (e.g., set content-encoding: gzip or vary: accept-encoding). The fallback then writes the literal plaintext 'Internal Server Error' while content-encoding: gzip is still set, so the client sees a corrupt body. Reset/strip compression-related headers (content-encoding, and any pre-set content-length) before sending the 500.
  3. this.status(500) throws if headers have already been sent in the meantime (e.g., concurrent write or the compression handler partially writing). Guard with !this._headersSent like other error paths in this file do.
🛠️ Suggested adjustment
-      this.compressionHandler
-        .compressBuffer(this.req, this, bodyBuffer)
-        .then((compressed) => this._sendInternal(compressed))
-        .catch(() => {
-          this.sending = false;
-          if (!this.finished && !this.aborted) {
-            this.status(500);
-            // Bypass compression for error response to avoid recursion
-            this._sendInternal('Internal Server Error');
-          }
-        });
+      this.compressionHandler
+        .compressBuffer(this.req, this, bodyBuffer)
+        .then((compressed) => this._sendInternal(compressed))
+        .catch((err) => {
+          // Log so failures aren't silently masked
+          // (use the framework logger if you have one wired in here)
+          // eslint-disable-next-line no-console
+          console.error('Response compression failed; falling back to 500', err);
+          this.sending = false;
+          if (this.finished || this.aborted || this._headersSent) return;
+          // Drop any headers the compression handler may have set so the
+          // plaintext fallback isn't tagged as gzip/br/etc.
+          this.removeHeader('content-encoding');
+          this.removeHeader('content-length');
+          this.status(500);
+          this._sendInternal('Internal Server Error');
+        });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/http/core/response.ts` around lines 1224 - 1272, The compression error
path in send() (where compressionHandler.compressBuffer(...) .catch(...))
swallows the error, can leave compression headers set and calls this.status(500)
unsafely; update the catch to log the original error via the configured logger,
clear any compression-related headers (at least 'content-encoding' and
'content-length' and optionally 'vary') before sending the fallback body, guard
the this.status(500) call with if (!this._headersSent) to avoid throwing, and
ensure this.sending is reset appropriately before calling
_sendInternal('Internal Server Error') so state remains consistent (refer to
methods/properties: send, compressionHandler.compressBuffer, _sendInternal,
this.status, this._headersSent, this.sending).
🧹 Nitpick comments (1)
src/http/platform/uws-platform.adapter.ts (1)

54-54: 💤 Low value

Prefer a top‑level import type for CompressionOptions (matches the rest of this file).

The inline import('../../http/handlers/compression/compression-handler').CompressionOptions works, but every other type used here (e.g., PlatformOptions, CorsOptions, RouteMetadata, StaticFileOptions) is brought in via top-level import type { ... } from '...'. The relative path is also unusually long given the file lives at src/http/platform/...; the canonical path is ../handlers/compression/compression-handler (already used elsewhere in this PR).

♻️ Suggested change
 import type { StaticFileOptions } from '../handlers/static/static-file-handler';
+import type { CompressionOptions } from '../handlers/compression/compression-handler';
@@
-  compress?: import('../../http/handlers/compression/compression-handler').CompressionOptions;
+  compress?: CompressionOptions;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/http/platform/uws-platform.adapter.ts` at line 54, Replace the inline
type reference for compress with a top-level type import: add import type {
CompressionOptions } from '../handlers/compression/compression-handler' (note
the canonical relative path) and change the property declaration to use
CompressionOptions directly; this matches the rest of the file's style and
removes the long inline import expression for CompressionOptions in
uws-platform.adapter.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/http/core/response.ts`:
- Around line 883-919: The _streamCompressed method sets this.activeStream =
readable but never registers the compressStream for abort cleanup, so when the
client aborts the compressStream (zlib) resources remain alive; update the code
to track and teardown compressStream on abort too — either set this.activeStream
to compressStream (or add a new this.activeCompressStream property) when
starting the stream in _streamCompressed and ensure the uwsRes.onAborted handler
(the existing abort cleanup that currently destroys this.activeStream) also
destroys the compressStream, and clear the tracked compressStream in the
onFinish/onError cleanup in _streamCompressed (references: _streamCompressed,
activeStream, compressStream, uwsRes.onAborted).
- Around line 1237-1253: The bug is that this.sending is set before
potentially-throwing serialization (JSON.stringify) in send(), which can leave
the Response locked; to fix, perform serialization of body (compute finalBody
via JSON.stringify or other branches) before setting this.sending, or wrap the
serialization in a try/catch that on any throw resets this.sending (and
rethrows) so the flag is not left true; locate the serialization block that
assigns finalBody and the this.sending assignment and either move the
this.sending = true below the serialization or add a try/finally around
serialization to clear sending on failure.

In `@src/http/interfaces/http-options.interface.ts`:
- Around line 147-164: The interface currently defines compress?:
CompressionOptions which mismatches the public docs that use compression; rename
the property to compression?: CompressionOptions in
src/http/interfaces/http-options.interface.ts and update any references/usages
(constructor args, destructuring, tests, consumers) that access the old compress
symbol so they read/write the new compression property, preserving the same
CompressionOptions type and JSDoc text/examples.

---

Outside diff comments:
In `@src/http/core/response.ts`:
- Around line 852-878: The stream() method currently flushes buffered write()
chunks via flushChunks() before determining compressibility, causing
mixed-encoding responses; change the order so you call
compressionHandler.createCompressionStream(this.req, this) before invoking
atomic()/flushChunks(), and if a compression stream is returned route any
buffered chunks (from flushChunks()/internal buffer) through the compression
stream and into _streamCompressed(readable, compressStream) rather than writing
them directly to uWS; if no compression stream is returned keep the existing
atomic()/flushChunks() path and call _streamFromReadable(readable, totalSize).
- Around line 1224-1272: The compression error path in send() (where
compressionHandler.compressBuffer(...) .catch(...)) swallows the error, can
leave compression headers set and calls this.status(500) unsafely; update the
catch to log the original error via the configured logger, clear any
compression-related headers (at least 'content-encoding' and 'content-length'
and optionally 'vary') before sending the fallback body, guard the
this.status(500) call with if (!this._headersSent) to avoid throwing, and ensure
this.sending is reset appropriately before calling _sendInternal('Internal
Server Error') so state remains consistent (refer to methods/properties: send,
compressionHandler.compressBuffer, _sendInternal, this.status,
this._headersSent, this.sending).

---

Nitpick comments:
In `@src/http/platform/uws-platform.adapter.ts`:
- Line 54: Replace the inline type reference for compress with a top-level type
import: add import type { CompressionOptions } from
'../handlers/compression/compression-handler' (note the canonical relative path)
and change the property declaration to use CompressionOptions directly; this
matches the rest of the file's style and removes the long inline import
expression for CompressionOptions in uws-platform.adapter.ts.
🪄 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: e685faff-d3af-4a2e-9ec2-109d1edb7b3f

📥 Commits

Reviewing files that changed from the base of the PR and between 1babb51 and 8d85ed8.

📒 Files selected for processing (4)
  • src/http/core/response.ts
  • src/http/interfaces/http-options.interface.ts
  • src/http/platform/uws-platform.adapter.ts
  • src/http/routing/route-registry.ts

Comment thread src/http/core/response.ts
Comment thread src/http/core/response.ts Outdated
Comment thread src/http/interfaces/http-options.interface.ts
@VikramAditya33 VikramAditya33 merged commit 1773fc0 into main May 11, 2026
4 checks passed
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.

HTTP response compression for send() and stream()

1 participant