Skip to content

fix: use precompiled gzip when streaming files#108

Open
vibhor-aggr wants to merge 2 commits into
koajs:masterfrom
vibhor-aggr:fix/precompiled-gzip-stream
Open

fix: use precompiled gzip when streaming files#108
vibhor-aggr wants to merge 2 commits into
koajs:masterfrom
vibhor-aggr:fix/precompiled-gzip-stream

Conversation

@vibhor-aggr

@vibhor-aggr vibhor-aggr commented Jun 11, 2026

Copy link
Copy Markdown

Closes #100.

usePrecompiledGzip previously only used the cached .gz file when the original response was buffered. With buffer: false, the middleware streamed the original file and dynamically piped it through gzip even when an adjacent precompiled .gz file existed.

This keeps the existing gzip eligibility checks, preserves buffered behavior, and adds a streaming path for the precompiled gzip file.

Verification:

  • ./node_modules/.bin/mocha --grep 'precompiled gzip when streaming dynamic files'
  • npm test
  • git diff --check

Summary by Sourcery

Serve precompiled gzip assets when streaming static files with gzip enabled.

New Features:

  • Add support for serving precompiled gzip files when static content is streamed instead of buffered.

Bug Fixes:

  • Ensure middleware uses existing precompiled gzip files instead of dynamically gzipping when streaming responses with usePrecompiledGzip enabled.

Tests:

  • Add regression test to verify precompiled gzip is used and dynamic gzip is not invoked when streaming dynamic files.

Summary by CodeRabbit

  • New Features

    • Static file handler now supports serving precompiled gzip-compressed assets, with automatic fallback to on-the-fly compression when precompiled versions are unavailable.
  • Tests

    • Added test coverage for precompiled gzip asset serving with streamed responses.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@vibhor-aggr, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 33 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dd07efcd-3ba7-4df4-a13e-37a495a42b58

📥 Commits

Reviewing files that changed from the base of the PR and between d23b3a6 and 8eb679a.

📒 Files selected for processing (2)
  • index.js
  • test/index.js
📝 Walkthrough

Walkthrough

This PR extends precompiled gzip support to streamed static responses. A new internal helper getPrecompiledGzip resolves precompiled .gz files from the in-memory cache or from disk via stat, returning metadata when found. The streamed response handler now checks options.usePrecompiledGzip and attempts to serve the precompiled variant with appropriate gzip headers and content-length. If not found, it falls back to the existing on-the-fly gzip compression. A new test validates this behavior by forcing dynamic gzip generation to fail and verifying that precompiled gzip content is correctly served with proper headers.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: adding support for serving precompiled gzip files during streaming mode, which is the core objective of the changeset.
Linked Issues check ✅ Passed The PR implements the exact requirement from issue #100: when options.buffer is false and options.usePrecompiledGzip is true, the middleware now serves the precompiled .gz file instead of dynamically generating compressed content.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objective: implementing a streaming path for precompiled gzip files. The additions to index.js (getPrecompiledGzip helper and streaming gzip handling) and test/index.js (verification test) are focused and aligned with the issue requirements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

@sourcery-ai

sourcery-ai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds support for serving precompiled .gz assets when responses are streamed (buffer: false), by detecting adjacent gzip files before creating dynamic gzip streams and by covering the behavior with a regression test.

Sequence diagram for serving precompiled gzip when streaming

sequenceDiagram
  participant Client
  participant KoaCtx as Koa_ctx
  participant StaticCache as staticCache_middleware
  participant FS as fs

  Client->>KoaCtx: HTTP GET /asset
  KoaCtx->>StaticCache: staticCache(dir, options, files)
  StaticCache->>StaticCache: [shouldGzip]
  StaticCache->>StaticCache: [options.usePrecompiledGzip]
  StaticCache->>StaticCache: getPrecompiledGzip(file, filename, files)
  alt [precompiledGzip found]
    StaticCache->>KoaCtx: set content-encoding gzip
    StaticCache->>KoaCtx: set length precompiledGzip.length
    alt [precompiledGzip.buffer]
      StaticCache->>KoaCtx: ctx.body = precompiledGzip.buffer
    else [no buffer]
      StaticCache->>FS: createReadStream(precompiledGzip.path)
      FS-->>StaticCache: ReadableStream
      StaticCache->>KoaCtx: ctx.body = ReadableStream
    end
  else [no precompiledGzip]
    StaticCache->>FS: createReadStream(file.path)
    FS-->>StaticCache: ReadableStream
    StaticCache->>KoaCtx: ctx.body = ReadableStream (dynamic gzip or plain)
  end
Loading

File-Level Changes

Change Details Files
Serve precompiled gzip files in the streaming path before falling back to dynamic gzip.
  • Introduced a precompiled gzip lookup that runs when gzip is enabled and usePrecompiledGzip is true, even for non-buffered responses.
  • When a matching precompiled gzip file is found, set the response content-encoding and length from the gzip metadata and stream either the cached buffer or a new file read stream as the response body.
  • Only create the original file read stream when no precompiled gzip is available, preserving existing gzip eligibility logic.
index.js
Add regression test to ensure precompiled gzip files are used when streaming dynamic files and dynamic gzip is not invoked.
  • Extended test dependencies to include mz/zlib and os, and used fs.mkdtempSync to create a temporary static directory for the test asset.
  • Created both plain and pre-gzipped versions of a test asset, then monkeypatched mzzlib.createGzip to throw if dynamic gzip is used, ensuring the middleware relies on the precompiled gzip file.
  • Asserted that the server responds with gzip content encoding, the precompiled gzip content length, and the original uncompressed content body when requesting the asset with gzip accepted.
test/index.js

Assessment against linked issues

Issue Objective Addressed Explanation
#100 Serve precompiled .gz static files (when available) instead of dynamically gzipping the file when options.buffer is false and options.usePrecompiledGzip is true.
#100 Add test coverage to ensure precompiled gzip files are used in the streaming (buffer: false) scenario and dynamic gzip is not invoked.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've left some high level feedback:

  • The test that overrides mzzlib.createGzip mutates a shared module-level singleton and only restores it in the .end callback; consider using a try/finally or afterEach hook to guarantee cleanup even if the test fails or throws earlier, to avoid leaking state into other tests.
  • In getPrecompiledGzip, await fs.stat(gzPath) assumes a promise-based fs API; if fs is Node’s core module here, this will never resolve as expected—either switch to the existing async wrapper you use elsewhere (e.g. mz/fs) or wrap fs.stat in a promise in this function.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The test that overrides `mzzlib.createGzip` mutates a shared module-level singleton and only restores it in the `.end` callback; consider using a `try/finally` or `afterEach` hook to guarantee cleanup even if the test fails or throws earlier, to avoid leaking state into other tests.
- In `getPrecompiledGzip`, `await fs.stat(gzPath)` assumes a promise-based `fs` API; if `fs` is Node’s core module here, this will never resolve as expected—either switch to the existing async wrapper you use elsewhere (e.g. `mz/fs`) or wrap `fs.stat` in a promise in this function.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@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: 1

🧹 Nitpick comments (2)
test/index.js (1)

373-418: ⚡ Quick win

Consider adding test coverage for edge cases.

The new test validates the happy path: precompiled gzip is correctly served in streaming mode when the .gz file exists. To strengthen confidence in the implementation, consider adding tests for:

  • Missing .gz file: Verify that when asset.js.gz does not exist, the middleware falls back to dynamically generating gzip (remove the createGzip mock or make it return a valid stream).
  • Cached precompiled gzip: Verify the code path where files.get(filename + '.gz') returns a cached file object (line 172 of index.js).
  • Stale .gz file (if mtime validation is added per the previous comment): Verify behavior when the .gz file is older than the original file.
🤖 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 `@test/index.js` around lines 373 - 418, Add tests covering the edge cases
around serving precompiled gzips: (1) "Missing .gz file" — create a test similar
to the existing one but do not write asset.js.gz and ensure the middleware falls
back to dynamic gzip by restoring/allowing mzzlib.createGzip to work (reference
mzzlib.createGzip and the staticCache invocation with gzip/usePrecompiledGzip
options); (2) "Cached precompiled gzip" — simulate a cached entry by ensuring
files.get(filename + '.gz') returns a file object (reference files.get and the
filename + '.gz' lookup in index.js) and assert the middleware serves that
cached object; (3) "Stale .gz file" — create asset.js and a gz file with an
older mtime than asset.js and assert expected behavior (fallback or re-gzip per
implementation). For each test, reuse the streaming/dynamic options
(buffer:false, dynamic:true) and assert proper Content-Encoding/Content-Length
and cleanup restores mzzlib.createGzip and removes tmp files.
index.js (1)

139-148: ⚖️ Poor tradeoff

ETag does not reflect the gzipped representation.

The ETag (set at line 94 from file.md5) represents the MD5 hash of the original uncompressed content. When serving the precompiled gzipped variant, the actual bytes differ, but the ETag remains unchanged. This extends the existing pattern from the buffered path (lines 122-137), where the same ETag is used for both compressed and uncompressed responses.

While the Vary: Accept-Encoding header (line 82) instructs caches to store separate entries per encoding, not all HTTP caches handle Vary correctly, and clients performing conditional requests may experience incorrect 304 responses if they switch between accepting and not accepting gzip.

Per HTTP semantics, the ETag should uniquely identify each representation, including differences in Content-Encoding. However, fixing this would require computing separate ETags for gzipped variants and updating cache invalidation logic across both buffered and streaming paths.

🤖 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 `@index.js` around lines 139 - 148, The ETag currently uses file.md5 (original
uncompressed content) even when serving precompiledGzip; compute and set a
distinct ETag for the gzipped representation before returning so responses
encoded with gzip have a unique validator. In the precompiledGzip branch (where
precompiledGzip is returned from getPrecompiledGzip), derive an MD5 (or other
strong hash) from the gzipped bytes (use precompiledGzip.buffer if present, or
stream/read precompiledGzip.path to compute the hash), then call ctx.set('etag',
computedGzipEtag) (instead of leaving file.md5) and keep the Vary header; ensure
the same approach is applied consistently with the buffered compressed path that
uses file.md5 so each Content-Encoding variant has its own ETag.
🤖 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 `@index.js`:
- Around line 171-189: In getPrecompiledGzip, after obtaining stats for the .gz
file (gzPath), also stat the original file (file.path) and compare modification
times; if the .gz mtime is older than the original file's mtime return null so
stale precompressed files are not used. Specifically, inside getPrecompiledGzip,
call fs.stat on file.path (e.g., origStats), compare gzStats.mtimeMs (or mtime)
with origStats.mtimeMs, and only return the { path: gzPath, length: stats.size }
when gz is equal or newer; otherwise return null. Ensure errors from
stat(file.path) propagate similarly to the current error handling for gzPath.

---

Nitpick comments:
In `@index.js`:
- Around line 139-148: The ETag currently uses file.md5 (original uncompressed
content) even when serving precompiledGzip; compute and set a distinct ETag for
the gzipped representation before returning so responses encoded with gzip have
a unique validator. In the precompiledGzip branch (where precompiledGzip is
returned from getPrecompiledGzip), derive an MD5 (or other strong hash) from the
gzipped bytes (use precompiledGzip.buffer if present, or stream/read
precompiledGzip.path to compute the hash), then call ctx.set('etag',
computedGzipEtag) (instead of leaving file.md5) and keep the Vary header; ensure
the same approach is applied consistently with the buffered compressed path that
uses file.md5 so each Content-Encoding variant has its own ETag.

In `@test/index.js`:
- Around line 373-418: Add tests covering the edge cases around serving
precompiled gzips: (1) "Missing .gz file" — create a test similar to the
existing one but do not write asset.js.gz and ensure the middleware falls back
to dynamic gzip by restoring/allowing mzzlib.createGzip to work (reference
mzzlib.createGzip and the staticCache invocation with gzip/usePrecompiledGzip
options); (2) "Cached precompiled gzip" — simulate a cached entry by ensuring
files.get(filename + '.gz') returns a file object (reference files.get and the
filename + '.gz' lookup in index.js) and assert the middleware serves that
cached object; (3) "Stale .gz file" — create asset.js and a gz file with an
older mtime than asset.js and assert expected behavior (fallback or re-gzip per
implementation). For each test, reuse the streaming/dynamic options
(buffer:false, dynamic:true) and assert proper Content-Encoding/Content-Length
and cleanup restores mzzlib.createGzip and removes tmp files.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 41cfa9d8-a5f5-4168-8456-320342f63ae0

📥 Commits

Reviewing files that changed from the base of the PR and between 182f260 and d23b3a6.

📒 Files selected for processing (2)
  • index.js
  • test/index.js

Comment thread index.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant