feat(static): add Last-Modified and ETag conditional caching - #269
Conversation
Every served file now carries `Last-Modified` (from mtime) and a weak `ETag`, and a matching `If-Modified-Since`/`If-None-Match` request is answered with an empty `304 Not Modified` before the body is read. - `If-None-Match` takes precedence over `If-Modified-Since` (RFC 9110 §13.2.2): present-and-unmatched is final. - The `ETag` is weak and folds in `Content-Encoding`, so brotli and gzip responses under one URL get distinct validators — which a cache keying on `Vary: Accept-Encoding` relies on. Weak because on-the-fly encodes are not byte-stable and no byte ranges are served. - New `lastModified` and `etag` options (both default `true`). - `renderHTML` routes carry neither: the rendered body is the caller's. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughChangesStatic cache validators
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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 |
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@docs/1.guide/4.middleware.md`:
- Line 112: Update the opening statement in the middleware documentation to
qualify that served files, excluding renderHTML routes, carry ETag and
Last-Modified headers. Keep the existing renderHTML clarification and all
validator behavior details unchanged.
In `@src/static.ts`:
- Around line 453-475: Update conditional request handling around
matchesIfNoneMatch: read the raw If-None-Match header regardless of etagValue,
let its presence suppress If-Modified-Since, and only evaluate the date
validator for GET/HEAD methods. For a matching If-None-Match, return 304 for
GET/HEAD and 412 for other configured methods; add coverage for ETag-disabled
requests with both headers and non-GET/HEAD behavior.
- Around line 446-449: Update the Last-Modified handling near lastModifiedMs to
clamp future file mtimes to the response origination time before converting them
to UTC. Preserve second-level precision and existing header behavior, and add a
regression test with a future-dated fixture verifying the emitted Last-Modified
is not in the future.
🪄 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
Run ID: 48364c4b-00cc-4312-ab3f-8e602eb54894
📒 Files selected for processing (3)
docs/1.guide/4.middleware.mdsrc/static.tstest/static.test.ts
Last-Modified and ETag conditional caching
- Cap `Last-Modified` at the response origination time so a future/skewed file mtime cannot 304 an `If-Modified-Since` until real time catches up (RFC 9110 §8.8.2). - Evaluate conditionals per RFC 9110 §13.2.2 precedence: a present `If-None-Match` suppresses `If-Modified-Since` regardless of the `etag` option (with ETags off, only `*` matches); a match answers GET/HEAD with `304` and any other configured method with `412`; `If-Modified-Since` is ignored for non-GET/HEAD (§13.1.3). - Qualify the docs: validators cover files served without `renderHTML`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds file-age and conditional-caching headers to
serveStatic(). Every served file now carriesLast-Modifiedand a weakETag, and a conditional request that still matches is answered with an empty304 Not Modifiedbefore the body is ever read.Last-Modifiedfrom the file's mtime, floored to second granularity to match HTTP-date precision.ETag— a weak validator (W/"<size>-<mtime>"). Weak because on-the-fly compression is not byte-stable and this middleware serves no byte ranges (the one advantage a strong tag would buy). The negotiatedContent-Encodingis folded in, so a brotli and a gzip response under one URL get distinct validators — which a shared cache keying onVary: Accept-Encodingrelies on.If-None-Match/If-Modified-Since→304.If-None-Matchtakes precedence (RFC 9110 §13.2.2): present-and-unmatched is final, andIf-Modified-Sinceis never consulted. The 304 carriesETag/Last-Modified/Varybut drops the representation headers.Options
Two new options, both default
true:lastModified— emitLast-Modifiedand honorIf-Modified-Since.etag— emitETagand honorIf-None-Match.renderHTMLroutes carry neither, since that body is the caller's to validate — consistent with how they are already excluded from compression.Implementation notes
ServableFilenow carriesmtimeMs, pulled from thefstatopenServablealready performs — no extra syscall.Tests
14 new cases: weak-ETag shape, 304 round-trips for both validators,
If-None-Matchprecedence over a freshIf-Modified-Since, per-encoding ETag distinctness (identity tag rejected on a gzip request), theetag: false/lastModified: falseopt-outs,renderHTMLexclusion, and conditional HEAD. All 125 static tests pass; lint and format clean.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
ETagandLast-Modifiedvalidators by default.If-None-MatchandIf-Modified-Since, returning304 Not Modifiedwhen appropriate.HEADrequests.Documentation