Skip to content

fix(serve-static): check the response (not request) for an existing content-length#1391

Merged
pi0 merged 2 commits into
h3js:mainfrom
francisjohnjohnston-web:pil-h3-static-content-length-res
Jul 2, 2026
Merged

fix(serve-static): check the response (not request) for an existing content-length#1391
pi0 merged 2 commits into
h3js:mainfrom
francisjohnjohnston-web:pil-h3-static-content-length-res

Conversation

@francisjohnjohnston-web

@francisjohnjohnston-web francisjohnjohnston-web commented May 23, 2026

Copy link
Copy Markdown
Contributor

What

In serveStatic (src/utils/static.ts), the content-length response header is set from meta.size
only when one isn't already present. That guard checks the request headers:

if (meta.size !== undefined && meta.size > 0 && !event.req.headers.get("content-length")) {
  event.res.headers.set("content-length", meta.size + "");
}

Every sibling guard in the same function checks the response headers instead — content-type,
content-encoding, and last-modified are all set only if (!event.res.headers.get(...)). This brings
content-length into line:

if (meta.size !== undefined && meta.size > 0 && !event.res.headers.get("content-length")) {

Why (asymmetric — easy to miss in review)

A GET/HEAD request almost never carries a content-length of its own, so
!event.req.headers.get("content-length") is "accidentally true" in normal use and the derived size
is still applied — the function looks correct in every ordinary test. The bug only surfaces in two edge
cases:

  1. A content-length deliberately set by the caller via options.headers is silently overwritten by
    meta.size (the request guard never sees the response header that's already there).
  2. A request that does carry a content-length (e.g. behind a proxy, or a GET with a body) wrongly
    suppresses the response content-length.

Because the common path is unaffected, a reviewer scanning the logic sees event.req.headers and
event.res.headers a line apart and the slip reads as intentional — until the three sibling guards are
lined up next to it.

Safety / blast radius

  • One-token change to internal logic; no public API, signature, or routing change.
  • Behaviour-preserving on the common path (no pre-set response content-length ⇒ identical result; all
    four existing content-length assertions in test/static.test.ts stay green).
  • Brings content-length into line with the function's own "set only when absent" contract.

Testing

  • Added a regression test: an explicit response content-length (via options.headers) must be
    preserved, not overwritten by meta.size. It fails on the current code (returns the derived 18
    instead of the explicit 999) on both the web and node matrix targets, and passes with the fix.
  • Full suite green with the fix: 1152 passed / 14 skipped, 0 type errors (vitest run + tsc --noEmit).

Summary by CodeRabbit

  • Bug Fixes

    • Improved static file responses to preserve an explicitly set content-length header instead of recalculating it.
  • Tests

    • Added coverage ensuring HEAD responses keep a configured content-length value unchanged.

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c72e61d-d3c6-4eba-a5c2-c2a8dea15918

📥 Commits

Reviewing files that changed from the base of the PR and between 4a1b971 and 75f04c2.

📒 Files selected for processing (2)
  • src/utils/static.ts
  • test/static.test.ts

📝 Walkthrough

Walkthrough

serveStatic now checks the response headers for an existing content-length before setting it from meta.size. A Vitest case verifies an explicitly configured content-length header stays unchanged on a HEAD request.

Changes

Static File Content-Length Handling

Layer / File(s) Summary
Content-Length Header Precedence
src/utils/static.ts, test/static.test.ts
serveStatic reads event.res.headers.get("content-length") instead of event.req.headers.get("content-length") before setting content-length when meta.size is present. A test verifies an explicit content-length header set via ServeStaticOptions.headers is preserved.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: pi0

Poem

🐰 I hopped through headers, quick and bright,
And checked the response side just right.
Your content-length stayed safe in place,
A tiny fix with graceful grace.
🥕

🚥 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 pull request title accurately summarizes the main change: fixing the bug by checking response headers instead of request headers for content-length in the serve-static utility.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

…ontent-length

serveStatic only sets content-length when one isn't already present, the
same way it guards content-type, content-encoding and last-modified. Those
three check event.res.headers, but the content-length guard checked
event.req.headers — the request's content-length, which has no bearing on the
response. A GET/HEAD request almost never carries a content-length, so the guard
is accidentally true in normal use and the derived size is still applied, which
is why the bug is invisible day to day. But a content-length deliberately set via
options.headers is silently overwritten by meta.size, and a request that does
carry a content-length wrongly suppresses the response header.

Switches the guard to event.res.headers, matching the sibling guards. Adds a
regression test asserting an explicit response content-length is preserved.
@francisjohnjohnston-web
francisjohnjohnston-web force-pushed the pil-h3-static-content-length-res branch from a70ab60 to 4a1b971 Compare May 25, 2026 08:01

@pi0x pi0x left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: this review was performed by AI agents (Claude Code) on behalf of the maintainer, in two independent passes.

Pass 1 reviewed the diff for correctness, edge cases, and test coverage. Pass 2 (a fresh agent, adversarial by design) independently re-read the diff, checked for merge conflicts/CI status, and tried to find a reason to block approval before this review was posted. Both passes agree this change is correct, adequately tested, and safe to merge.

Caveat: this repo's real CI workflow (lint + build + vitest) requires maintainer approval to run on fork PRs and has not executed on this PR yet — only automated third-party checks (CodeRabbit, Socket Security) have run. Please trigger/approve the CI workflow before merging.

@pi0
pi0 merged commit 7ae7dd1 into h3js:main Jul 2, 2026
4 of 5 checks passed
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

3 participants