Skip to content

🐛 fix(limiter): correct fixed-window hit credit on skipped requests - #4422

Merged
ReneWerner87 merged 10 commits into
mainfrom
claude/bug-fixes-gg4i5j
Jun 17, 2026
Merged

ReneWerner87 merged 10 commits into
mainfrom
claude/bug-fixes-gg4i5j

Conversation

@gaby

@gaby gaby commented Jun 11, 2026

Copy link
Copy Markdown
Member

When SkipSuccessfulRequests/SkipFailedRequests credits a hit back after
the handler runs, the fixed-window strategy decremented currHits
unconditionally and bumped a stale local counter for the
X-RateLimit-Remaining header. If the window rolled over (or another
request reset the entry) between the increment and the credit, currHits
could underflow below zero, inflating the next window's allowance, and
the header reported a value disconnected from stored state.

Mirror the sliding-window semantics: only credit the hit when the entry
still belongs to the same window and currHits is positive, and recompute
remaining from the fresh entry instead of incrementing the stale local.

claude added 3 commits June 10, 2026 22:50
When SkipSuccessfulRequests/SkipFailedRequests credits a hit back after
the handler runs, the fixed-window strategy decremented currHits
unconditionally and bumped a stale local counter for the
X-RateLimit-Remaining header. If the window rolled over (or another
request reset the entry) between the increment and the credit, currHits
could underflow below zero, inflating the next window's allowance, and
the header reported a value disconnected from stored state.

Mirror the sliding-window semantics: only credit the hit when the entry
still belongs to the same window and currHits is positive, and recompute
remaining from the fresh entry instead of incrementing the stale local.

https://claude.ai/code/session_015UgiFK9CxncaL1ZbevmkJ6
Sessions acquired via Store.Get are pooled and must be released per the
documented contract (Session.Release), but two paths never did:

- csrf sessionManager.getRaw/setRaw/delRaw acquired a session from the
  store (when no session middleware is active) and dropped it without
  Release, so every CSRF request with session storage lost the pooled
  object.
- the session middleware skipped saveSession — the only place the
  session was released — for destroyed sessions, so every Destroy()
  (e.g. logout) leaked the Session out of the pool.

https://claude.ai/code/session_015UgiFK9CxncaL1ZbevmkJ6
The previous comparison only handled a single entity tag and required
the weak indicator to match between client and server:

- 'If-None-Match: W/"a", "b"' against ETag '"a"' returned 200
  instead of 304 because the whole list was compared as one tag.
- a strong client tag never matched a weak server tag (and vice versa
  unless the list happened to substring-match), violating the weak
  comparison REQUIRED for If-None-Match (RFC 9110 §8.8.3.2).
- 'If-None-Match: *' was not recognized.
- strong matching relied on bytes.Contains substring semantics.

Parse the header as a comma-separated entity-tag list, compare each tag
with weak comparison (ignoring W/ prefixes, requiring quoted tags), and
support the wildcard. Also emit the ETag header on 304 responses as
required by RFC 9110 §15.4.5.

https://claude.ai/code/session_015UgiFK9CxncaL1ZbevmkJ6
@gaby
gaby requested a review from a team as a code owner June 11, 2026 02:56
@gaby gaby changed the title fix(limiter): correct fixed-window hit credit on skipped requests 🐛 fix(limiter): correct fixed-window hit credit on skipped requests Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 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 fixes resource lifecycle and correctness across middlewares: CSRF and session middleware properly release pooled resources, ETag matching implements RFC 9110 weak-tag comparison with new parsing helpers and tests, and rate limiters prevent negative remaining values across window boundaries and skip-success credit-back scenarios.

Changes

Middleware Resource Lifecycle & RFC Compliance Fixes

Layer / File(s) Summary
Session resource lifecycle release
middleware/csrf/session_manager.go, middleware/session/middleware.go
CSRF session manager defers Release() on sessions in getRaw, setRaw, and delRaw. Session middleware skips saveSession() for destroyed sessions and releases them back to the pool instead.
ETag RFC 9110 weak-tag matching
middleware/etag/etag.go, middleware/etag/etag_test.go
Response ETag is set before evaluating If-None-Match. New isNoneMatch helper parses/trims entries with wildcard and list support; etagWeakMatch implements RFC 9110 weak comparison with W/ prefix stripping and quote validation. Test_ETag_WeakComparison covers weak/strong combinations and unquoted scenarios.
Rate limiter remaining value clamping
middleware/limiter/limiter_fixed.go, middleware/limiter/limiter_sliding.go, middleware/limiter/limiter_test.go
Fixed-window limiter captures window expiration and only credits back if entry belongs to same window; recalculates remaining as maxRequests - currHits and clamps to 0. Sliding-window limiter clamps remaining to 0 after rate computation. Test verifies credit-back does not underflow after window rollover.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • gofiber/fiber#3899: Both PRs modify limiter fixed and sliding window implementations to adjust remaining/hit-counter recalculation across skip and rollover scenarios.
  • gofiber/fiber#3893: Both PRs adjust remaining clamping in middleware/limiter/limiter_sliding.go to prevent negative rate-limit header values.
  • gofiber/fiber#3016: Both PRs modify session middleware's lifecycle handling and resource cleanup for destroyed sessions.

Suggested labels

codex

Suggested reviewers

  • sixcolors
  • ReneWerner87
  • efectn

Poem

🐰 Sessions freed with careful defer,
ETags matched just as they occur,
Windows roll and limits stay,
No negative values lead astray,
Fiber's middleware hops with cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description clearly explains the problem, root cause, and solution, but does not follow the provided template structure with required sections like 'Changes introduced' and 'Checklist'. Restructure the description to follow the repository's template, including 'Changes introduced', 'Type of change' checkboxes, and 'Checklist' items to ensure consistency with repository standards.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the specific bug being fixed in the fixed-window rate limiter's hit credit logic when requests are skipped.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% 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
  • Commit unit tests in branch claude/bug-fixes-gg4i5j

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.

@ReneWerner87 ReneWerner87 added this to v3 Jun 11, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jun 11, 2026
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.52%. Comparing base (e187cad) to head (35d6a76).

Files with missing lines Patch % Lines
middleware/limiter/limiter_fixed.go 66.66% 1 Missing and 1 partial ⚠️
middleware/limiter/limiter_sliding.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4422      +/-   ##
==========================================
+ Coverage   91.50%   91.52%   +0.02%     
==========================================
  Files         134      134              
  Lines       13472    13497      +25     
==========================================
+ Hits        12327    12353      +26     
+ Misses        727      725       -2     
- Partials      418      419       +1     
Flag Coverage Δ
unittests 91.52% <88.88%> (+0.02%) ⬆️

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.

Switch bytes.TrimSpace to the generic utils.TrimSpace and replace the
wildcard bytes.Equal with a direct byte check. Tag bytes still use
bytes.Equal: utils only provides EqualFold, and entity-tag comparison
is octet-by-octet per RFC 9110.

https://claude.ai/code/session_015UgiFK9CxncaL1ZbevmkJ6

@ReneWerner87 ReneWerner87 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed with focus on correctness, concurrency, and RFC semantics. All four fixes are real and correctly implemented: the fixed-window credit guard closes a genuine currHits underflow (rate-limit bypass), the csrf/session Release calls fix session pool leaks without double-release risk, and the new If-None-Match handling matches RFC 9110 §8.8.3.2. Affected suites pass, including with -race.

A few remarks inline. Two notes that do not fit a specific line:

  • Behavior changes worth a changelog entry: If-None-Match: * now returns 304 (was 200), a strong client tag now matches a weak server tag, and the ETag header is now also sent on 304 responses. All RFC-correct, but observable.
  • Pre-existing, not for this PR: the fixed-window skip path still emits the pre-handler X-RateLimit-Reset value, while the sliding window recomputes it post-handler.

Comment thread middleware/limiter/limiter_fixed.go
Comment thread middleware/limiter/limiter_fixed.go
Comment thread middleware/etag/etag.go
Comment thread middleware/csrf/session_manager.go
ReneWerner87 and others added 2 commits June 14, 2026 21:33
Addresses review feedback on the fixed-window hit-credit fix:

- Add Test_Limiter_Fixed_Window_SkipSuccessfulRequests_DoesNotCreditNextWindow,
  a deterministic regression test analogous to the sliding-window one. It fails
  on the pre-fix code (slow skipped request wrongly credits the rolled window)
  and passes with the window guard.
- Clamp X-RateLimit-Remaining to 0 in both fixed and sliding strategies so a
  skip-credit path never emits a negative value to header-parsing clients.
- Document the comma-in-opaque-tag fail-open limitation in etag isNoneMatch.
- Document why csrf getRaw may return token.Raw after Release.

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

gaby commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

@ReneWerner87 the linter is failing on this one

gaby and others added 3 commits June 16, 2026 21:01
CI's golangci-lint restored a stale cache whose revive results predated
this function gaining its 'secure' scheme filter, so nolintlint reported
the //nolint:revive directive as unused even though revive's
flag-parameter rule fires on a fresh analysis.

Move the directive inline onto the function-declaration line (the exact
position of the finding) and touch the file so the linter re-analyzes it
instead of reusing the stale cached result. Verified with a clean
golangci-lint cache: revive flags the parameter and the directive
suppresses it, leaving 0 issues.

https://claude.ai/code/session_015UgiFK9CxncaL1ZbevmkJ6
Add a unit test table for the unexported etagWeakMatch helper, exercising
the weak/strong cross-comparison matrix and the malformed-tag rejection
branches (unquoted or empty client/server tags). These guard clauses are
unreachable through the middleware because Fiber always emits well-formed
quoted ETags, so a direct test is the appropriate way to cover them.

https://claude.ai/code/session_015UgiFK9CxncaL1ZbevmkJ6
@gaby

gaby commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

@ReneWerner87 the linter is failing on this one

Should be good now.

@ReneWerner87
ReneWerner87 enabled auto-merge June 17, 2026 03:59
@ReneWerner87
ReneWerner87 disabled auto-merge June 17, 2026 03:59
@ReneWerner87
ReneWerner87 merged commit bdd117c into main Jun 17, 2026
19 of 20 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/bug-fixes-gg4i5j branch June 17, 2026 03:59
@github-project-automation github-project-automation Bot moved this to Done in v3 Jun 17, 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