Skip to content

🐛 fix(ctx): evaluate If-Modified-Since when If-None-Match is absent in Fresh#4488

Merged
ReneWerner87 merged 7 commits into
gofiber:mainfrom
0xghost42:fix/fresh-if-modified-since-only
Jul 1, 2026
Merged

🐛 fix(ctx): evaluate If-Modified-Since when If-None-Match is absent in Fresh#4488
ReneWerner87 merged 7 commits into
gofiber:mainfrom
0xghost42:fix/fresh-if-modified-since-only

Conversation

@0xghost42

Copy link
Copy Markdown
Contributor

Description

Ctx.Fresh() (and Stale(), which is !Fresh()) never evaluated If-Modified-Since unless If-None-Match was also present on the request.

In req.go the if-modified-since date comparison was nested inside the if-none-match block, so a conditional request that sent only If-Modified-Since skipped the check entirely and fell through to return true. A resource whose Last-Modified is newer than the client's cached date was reported as still fresh — driving the caller to answer 304 Not Modified and leaving the client holding stale content.

This diverged from the reference implementation named in the method's own doc comment (https://github.com/jshttp/fresh/blob/master/index.js#L33), where the if (modifiedSince) block is a sibling of if (noneMatch) and runs independently. RFC 9110 §13.1.3 likewise expects If-Modified-Since to be evaluated when it is the only validator present.

Changes

  • De-nest the if-modified-since block in Fresh() so it is validated independently of if-none-match, mirroring jshttp/fresh: a missing or unparseable Last-Modified, or a Last-Modified newer than the client's date, marks the copy stale.
  • Add Test_Ctx_Fresh_ModifiedSinceOnly covering the If-Modified-Since-only paths (resource newer → stale, equal → fresh, older → fresh, no Last-Modified → stale). These paths were previously unexercised — every If-Modified-Since assertion in Test_Ctx_Fresh already had If-None-Match set on the same request, so the buggy branch was never reached.

The existing Test_Ctx_Fresh assertions are unchanged and still pass; the both-validators-present behavior is preserved.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Notes

Scoped deliberately to the undisputed If-Modified-Since-only case. For the both-validators-present case, jshttp/fresh ANDs the two validators whereas RFC 9110 §13.1.3 says If-None-Match takes precedence; I left that behavior as-is to keep this a clean, non-debatable fix. Happy to follow up on the precedence question separately if maintainers want to align fully with one or the other.

Fresh() nested the If-Modified-Since date comparison inside the
If-None-Match branch, so a conditional request carrying only
If-Modified-Since skipped the check entirely and always reported the
response as fresh. A resource modified after the client's cached copy
was therefore treated as unchanged (driving a 304 and leaving the
client with stale content).

De-nest the block so If-Modified-Since is validated independently of
If-None-Match, mirroring the referenced jshttp/fresh implementation: a
missing or unparseable Last-Modified, or a Last-Modified newer than the
client's date, marks the copy stale.

Add Test_Ctx_Fresh_ModifiedSinceOnly covering the If-Modified-Since-only
cases, which were previously unexercised.
@0xghost42 0xghost42 requested a review from a team as a code owner July 1, 2026 10:52
@welcome

welcome Bot commented Jul 1, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR updates HTTP freshness evaluation for If-None-Match and If-Modified-Since, adds coverage for those cases in ctx_test.go, and makes one cache middleware test deterministic by replacing sleeps with a controllable clock.

Changes

Fresh() Header Precedence Fix

Layer / File(s) Summary
If-None-Match wildcard precedence
req.go, ctx_test.go
Fresh() now returns true immediately for If-None-Match: *, and Test_Ctx_Fresh adds assertions for that precedence over date-based checks.
If-Modified-Since logic and coverage
req.go, ctx_test.go
Fresh() now returns false when Last-Modified is missing or either date cannot be parsed, and Test_Ctx_Fresh_ModifiedSinceOnly covers newer, equal, older, missing, and malformed date cases.

Deterministic cache timing test

Layer / File(s) Summary
Clock-driven cache timing
middleware/cache/cache_test.go
The cache middleware test uses testClock, advances time explicitly, and checks freshness before and after the s-maxage window without sleeping.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • gofiber/fiber#3150: Both PRs adjust Fresh() behavior around Last-Modified and If-Modified-Since comparison outcomes.
  • gofiber/fiber#3687: Both PRs modify DefaultReq.Fresh() in req.go for If-None-Match and date-based freshness handling.
  • gofiber/fiber#4430: Both PRs make cache freshness tests deterministic by using an injected clock instead of real-time sleeping.

Suggested labels: 📜 RFC Compliance

Suggested reviewers: sixcolors, efectn, ReneWerner87

Poem

A rabbit hops through header grass,
Wildcards leap and let dates pass,
Old clocks sleep? No, time obeys,
Freshness tested in clockwork ways,
Hop-hop—RFC shines today 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing Fresh() to evaluate If-Modified-Since when If-None-Match is absent.
Description check ✅ Passed The description is detailed and covers the bug, fix, tests, type of change, and notes; only the issue reference and checklist items are omitted.
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.
✨ 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.

@ReneWerner87 ReneWerner87 added this to v3 Jul 1, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

1 similar comment
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
ctx_test.go (1)

2637-2664: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the malformed-date branches here too.

The production change now treats unparseable Last-Modified and unparseable If-Modified-Since as stale, but this new If-Modified-Since-only test only covers the missing-header branch.

Suggested additional cases
 	// If-Modified-Since present but no Last-Modified response header: stale.
 	c = app.AcquireCtx(&fasthttp.RequestCtx{})
 	c.Request().Header.Set(HeaderIfModifiedSince, "Wed, 21 Oct 2015 07:28:00 GMT")
 	require.False(t, c.Fresh())
+
+	// Last-Modified present but unparseable: stale.
+	c = app.AcquireCtx(&fasthttp.RequestCtx{})
+	c.Request().Header.Set(HeaderIfModifiedSince, "Wed, 21 Oct 2015 07:28:00 GMT")
+	c.Response().Header.Set(HeaderLastModified, "invalid")
+	require.False(t, c.Fresh())
+
+	// If-Modified-Since unparseable: stale.
+	c = app.AcquireCtx(&fasthttp.RequestCtx{})
+	c.Request().Header.Set(HeaderIfModifiedSince, "invalid")
+	c.Response().Header.Set(HeaderLastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
+	require.False(t, c.Fresh())
 }
🤖 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 `@ctx_test.go` around lines 2637 - 2664, Extend
Test_Ctx_Fresh_ModifiedSinceOnly to cover the malformed-date paths in Ctx.Fresh
as well as the missing-header case. Add assertions for an invalid
HeaderLastModified value and an invalid HeaderIfModifiedSince value, ensuring
the test verifies these unparseable dates are treated as stale. Use the existing
Ctx.Fresh setup with app.AcquireCtx, HeaderIfModifiedSince, and
HeaderLastModified so the new cases stay alongside the current freshness
scenarios.
🤖 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.

Nitpick comments:
In `@ctx_test.go`:
- Around line 2637-2664: Extend Test_Ctx_Fresh_ModifiedSinceOnly to cover the
malformed-date paths in Ctx.Fresh as well as the missing-header case. Add
assertions for an invalid HeaderLastModified value and an invalid
HeaderIfModifiedSince value, ensuring the test verifies these unparseable dates
are treated as stale. Use the existing Ctx.Fresh setup with app.AcquireCtx,
HeaderIfModifiedSince, and HeaderLastModified so the new cases stay alongside
the current freshness scenarios.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 08617dbd-1e57-437d-9e7f-6c13d8338a0a

📥 Commits

Reviewing files that changed from the base of the PR and between 3f26054 and f922867.

📒 Files selected for processing (2)
  • ctx_test.go
  • req.go

@gaby gaby changed the title fix(ctx): evaluate If-Modified-Since when If-None-Match is absent in Fresh 🐛 fix(ctx): evaluate If-Modified-Since when If-None-Match is absent in Fresh Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.96%. Comparing base (3f26054) to head (e618f63).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4488      +/-   ##
==========================================
+ Coverage   92.90%   92.96%   +0.05%     
==========================================
  Files         138      138              
  Lines       13595    13609      +14     
==========================================
+ Hits        12631    12651      +20     
+ Misses        597      592       -5     
+ Partials      367      366       -1     
Flag Coverage Δ
unittests 92.96% <100.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gaby gaby requested review from a team and Copilot July 1, 2026 13:30

Copilot AI 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.

Pull request overview

Fixes a correctness bug in DefaultReq.Fresh()/Ctx.Fresh() where If-Modified-Since was only evaluated when If-None-Match was also present, causing some conditional requests to be treated as fresh incorrectly and potentially returning 304 Not Modified for stale client caches.

Changes:

  • De-nests If-Modified-Since evaluation in Fresh() so it runs independently of If-None-Match, aligning with the referenced jshttp/fresh logic.
  • Adds a focused test to cover If-Modified-Since-only request scenarios (newer/equal/older/missing Last-Modified).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
req.go Fixes Fresh() so If-Modified-Since is validated even when If-None-Match is absent.
ctx_test.go Adds regression coverage for If-Modified-Since-only behavior in Ctx.Fresh().

Comment thread req.go Outdated
Comment thread req.go
ReneWerner87 and others added 3 commits July 1, 2026 15:48
- req.go: trim the if-modified-since comment to a terse one-liner (the previous
  block claimed to mirror jshttp/fresh, which only holds for the if-none-match
  absent path) and bind a local response pointer to match the if-none-match
  branch.
- ctx_test.go: cover the unparseable Last-Modified and unparseable
  If-Modified-Since branches, which were stale-on-parse-error but untested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align Fresh() fully with the referenced jshttp/fresh and RFC 9110: when
If-None-Match is present, it decides freshness on its own (wildcard or ETag
match -> fresh, otherwise stale) and If-Modified-Since is not consulted. The
previous de-nesting evaluated both validators, which made a matching (or "*")
If-None-Match report stale whenever a date header disagreed - sending 200
instead of 304.

If-Modified-Since is now only reached when If-None-Match is absent; its
independent evaluation for the date-only case (the original bug fix) is
unchanged. Update Test_Ctx_Fresh to assert the precedence behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Unparseable" is flagged by the project spell checker; use "malformed".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test_CacheSMaxAgeOverridesMaxAgeWhenLonger used real time.Sleep plus a
whole-second alignment spin-loop. Cache freshness is computed on whole-second
Unix timestamps, so a sleep-based test flakes when the caching request lands
late in its second or a sleep overshoots under load (-race). Advance a
deterministic clock instead, mirroring the sibling WhenShorter test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Modified

The common conditional request sends If-Modified-Since equal to the exact
Last-Modified the client received. Last-Modified is already parsed and
validated first, so when the two byte slices are equal the dates are identical
and fresh: skip the second ParseHTTPDate and the comparison. Cuts this path
from two parses to one (~250ns -> ~156ns) with no behavior change - a malformed
Last-Modified still fails the first parse and reports stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ReneWerner87 ReneWerner87 merged commit 05919e5 into gofiber:main Jul 1, 2026
17 checks passed
@github-project-automation github-project-automation Bot moved this to Done in v3 Jul 1, 2026
@ReneWerner87 ReneWerner87 modified the milestones: v3, v3.4.0 Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants