Skip to content

🐛 bug: Fix and improvements for the sliding window Limiter#3893

Merged
ReneWerner87 merged 7 commits intomainfrom
update-rate-limit-to-use-dynamic-maxrequests
Nov 24, 2025
Merged

🐛 bug: Fix and improvements for the sliding window Limiter#3893
ReneWerner87 merged 7 commits intomainfrom
update-rate-limit-to-use-dynamic-maxrequests

Conversation

@gaby
Copy link
Member

@gaby gaby commented Nov 23, 2025

Summary

  • This pull request provides a comprehensive fix for several issues in the sliding window limiter. It correctly implements support for dynamic request limits using MaxFunc, which was a critical bug. The logic for window rotation, calculating request rates, and handling skipped requests has been significantly improved for correctness, especially in scenarios involving long-running requests or idle periods. Additionally, the introduction of overflow checks for TTL calculation makes the middleware more robust.

Fixes #3867

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 23, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Implements sliding-window rotation and TTL helpers; preserves and shifts previous/current buckets on rotation; recalculates rate and remaining using per-request maxRequests (from MaxFunc); fixes skip-hit race by updating the original entry reference; and adds tests covering header behavior, dynamic max, window expiration, and skip semantics.

Changes

Cohort / File(s) Summary
Core limiter logic
middleware/limiter/limiter_sliding.go
Adds internal helpers rotateWindow, bucketForOriginalHit, ttlDuration, secondsToDuration; implements rotation-aware transfer/expiration of prevHitscurrHits; computes TTL via ttlDuration; uses per-request maxRequests for rate and remaining; refactors SkipHit path to use the original entry reference (avoiding re-fetch race), recompute weight/rate/remaining, persist with computed TTL, and update headers/Retry-After accordingly.
Tests
middleware/limiter/limiter_test.go
Adds/updates tests for headers-disabled behavior, MaxFunc overriding static Max, window recalculation after handler delays, expiration of stale previous-window hits, and SkipFailedRequests decrement behavior; imports strconv for header assertions.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Limiter
  participant Storage
  participant Handler

  Client->>Limiter: HTTP request (key)
  Limiter->>Storage: get(key)
  Storage-->>Limiter: entry {ts, prevHits, currHits, exp}
  note right of Limiter `#f0f4c3`: rotateWindow(now, expiration)\ncompute resetInSec, weight, expire/shift prev→curr
  Limiter->>Limiter: maxRequests := cfg.MaxFunc(c)\nrate := int(prevHits*weight)+currHits\nremaining := maxRequests - rate
  alt Allowed
    Limiter->>Storage: set(entry with currHits+1, ttlDuration(resetInSec, expiration))
    Limiter->>Handler: call next()
    Handler-->>Limiter: response (status)
    alt Should Skip Hit (SkipFailed/SkipSuccessful)
      note right of Limiter `#e8f5e9`: Use original entry reference\nrecompute weight/rate/remaining\ndecrement correct bucket\nLimiter->>Storage: set(updated entry, ttlDuration(...))
    end
    Limiter-->>Client: Response + X-RateLimit-* headers (unless disabled)
  else LimitReached
    Limiter-->>Client: 429 + Retry-After (resetInSec)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • Concurrency and lock/unlock semantics around SkipHit paths and use of the original entry reference.
    • Correctness of rotateWindow math (weight, prev→curr transition) and TTL calculations.
    • Header values and Retry-After alignment with recalculated resetInSec.
    • New timing-sensitive tests for flakiness.

Suggested reviewers

  • sixcolors
  • efectn
  • ReneWerner87

Poem

🐇 I hopped through windows, small and wide,
I turned the ticks so counts abide.
MaxFunc guides each bounding light,
I nudge the hits and keep them right.
Hooray — the limiter sleeps tonight!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ⚠️ Warning The pull request description is too minimal and lacks required template sections such as 'Changes introduced', 'Type of change', checklist items, and detailed documentation of the implementation. Complete the PR description by filling out all required template sections: list specific changes introduced, select the type of change(s), confirm all checklist items have been addressed, and provide details on benchmarks, documentation updates, and tests added.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses all three critical bugs from issue #3867: eliminates race condition via rotation logic, uses dynamic maxRequests consistently, and properly recalculates remaining with window weight; comprehensive test coverage validates these fixes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the three bugs identified in issue #3867; no unrelated modifications or extraneous functionality were introduced in the limiter or test files.
Title check ✅ Passed The title mentions 'bug: Fix and improvements for the sliding window Limiter' which accurately describes the main changes—repairs to the sliding window limiter's behavior with dynamic max values and skip request handling.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update-rate-limit-to-use-dynamic-maxrequests

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @gaby, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue in the sliding window rate limiter where it did not fully respect dynamically configured maximum request limits. The changes ensure that the limiter consistently uses the MaxFunc value for capacity calculations and header updates, improving the accuracy and flexibility of rate limiting. A new test case has been added to confirm this behavior.

Highlights

  • Dynamic MaxFunc Integration: The sliding window limiter now correctly uses the dynamic MaxFunc value to determine the remaining capacity, rather than relying solely on the static Max configuration.
  • Accurate Remaining Allowance Calculation: The system now recomputes the remaining window allowance and refreshes rate-limit headers after requests are skipped, ensuring accurate rate limiting even in edge cases.
  • Regression Test Added: A new regression test has been introduced to validate that MaxFunc properly overrides the static Max value, verifying both enforcement and header updates.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ReneWerner87 ReneWerner87 added this to v3 Nov 23, 2025
@ReneWerner87 ReneWerner87 added this to the v3 milestone Nov 23, 2025
@gaby gaby changed the title Fix sliding limiter to respect dynamic max 🐛 bug: Fix sliding limiter to respect dynamic max Nov 23, 2025
@codecov
Copy link

codecov bot commented Nov 23, 2025

Codecov Report

❌ Patch coverage is 67.85714% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.68%. Comparing base (db2576e) to head (6d7dde9).
⚠️ Report is 47 commits behind head on main.

Files with missing lines Patch % Lines
middleware/limiter/limiter_sliding.go 67.85% 10 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3893      +/-   ##
==========================================
- Coverage   91.69%   91.68%   -0.02%     
==========================================
  Files         115      117       +2     
  Lines        9819    10030     +211     
==========================================
+ Hits         9004     9196     +192     
- Misses        516      530      +14     
- Partials      299      304       +5     
Flag Coverage Δ
unittests 91.68% <67.85%> (-0.02%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses the issue of the sliding window limiter not respecting the dynamic MaxFunc. The changes correctly use the dynamic maxRequests value for calculating the remaining capacity. The logic for handling skipped requests is also properly updated to recalculate the rate, which is a significant improvement over the previous implementation. The addition of a regression test is excellent, as it clearly verifies the intended behavior. I have one suggestion to further improve the accuracy of the rate calculation when requests are skipped.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug in the sliding window rate limiter where it was using the static cfg.Max value instead of respecting the dynamic MaxFunc value when calculating remaining capacity. The fix ensures that both the initial capacity check and the recalculation after skipped requests properly use the dynamic maximum.

Key Changes

  • Updated the sliding window limiter to use maxRequests (derived from MaxFunc) instead of cfg.Max for capacity calculations
  • Added logic to recompute rate and remaining after decrementing hits for skipped requests, ensuring headers reflect accurate values
  • Added a regression test that verifies MaxFunc properly overrides the static Max value in both enforcement and response headers

Reviewed changes

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

File Description
middleware/limiter/limiter_sliding.go Fixed capacity calculation to use dynamic maxRequests instead of static cfg.Max, and added recalculation of rate/remaining after skipped requests
middleware/limiter/limiter_test.go Added regression test that verifies MaxFunc overrides static Max value with proper enforcement and header values

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
middleware/limiter/limiter_sliding.go (1)

139-166: Recomputing rate/remaining after skipped requests keeps headers accurate (optionally clamp for safety)

Recomputing rate and remaining after e.currHits-- in the skip branch ensures that X-RateLimit-Remaining reflects the decremented hit count instead of the pre-skip value, which aligns with the intent of Skip*Requests. In very concurrent scenarios, the recomputed remaining used only for headers could theoretically go negative due to other requests updating the same key; if you want to guard against that, you could optionally clamp just for header output, e.g. if remaining < 0 { remaining = 0 }, without changing enforcement logic (which still happens before c.Next).

middleware/limiter/limiter_test.go (1)

9-9: Regression test cleanly covers dynamic MaxFunc overriding static Max

The added strconv import and Test_Limiter_Sliding_MaxFuncOverridesStaticMax look solid:

  • MaxFunc returning a smaller dynamicMax than staticMax verifies that SlidingWindow enforcement and X-RateLimit-Limit / X-RateLimit-Remaining are governed by the dynamic value as intended.
  • The 3-request sequence (OK/OK/429) matches the sliding-window math with dynamicMax == 2, and the header assertions ensure the remaining value is also computed from dynamicMax.
  • Use of require from testify is consistent with the project’s testing style. Based on learnings, this matches existing conventions.

If you want to match surrounding tests, you could optionally add t.Parallel() at the start of this test, but it’s not required.

Also applies to: 414-446

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5caa478 and b8eabb4.

📒 Files selected for processing (2)
  • middleware/limiter/limiter_sliding.go (3 hunks)
  • middleware/limiter/limiter_test.go (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-16T07:19:52.418Z
Learnt from: grivera64
Repo: gofiber/fiber PR: 3807
File: adapter_test.go:118-144
Timestamp: 2025-10-16T07:19:52.418Z
Learning: In the Fiber codebase, the linter does not allow `require` assertions from within HTTP handlers (including net/http-style handlers). Use `t.Fatalf`, `t.Errorf`, or similar `testing.T` methods for error handling inside handler functions instead.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-08T19:06:06.583Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 2922
File: middleware/cors/utils.go:63-71
Timestamp: 2024-10-08T19:06:06.583Z
Learning: The project uses the testify/assert package for assertions in unit tests.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-02T23:03:31.727Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/store.go:164-167
Timestamp: 2024-10-02T23:03:31.727Z
Learning: Unit tests in this project use testify require.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-12-13T08:14:22.851Z
Learnt from: efectn
Repo: gofiber/fiber PR: 3162
File: hooks_test.go:228-228
Timestamp: 2024-12-13T08:14:22.851Z
Learning: In Go test files, prefer using the `require` methods from the `testify` package for assertions instead of manual comparisons and calls to `t.Fatal` or `t.Fatalf`.

Applied to files:

  • middleware/limiter/limiter_test.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Upload results
  • GitHub Check: Compare
  • GitHub Check: lint
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: unit (1.25.x, macos-latest)
  • GitHub Check: repeated
🔇 Additional comments (1)
middleware/limiter/limiter_sliding.go (1)

85-132: Dynamic maxRequests correctly drives remaining and limit enforcement

Using remaining := maxRequests - rate and checking if remaining < 0 before calling cfg.LimitReached correctly ties the effective limit and remaining capacity to the per-request MaxFunc value, and keeps it consistent with X-RateLimit-Limit (which already uses maxRequests). This fixes the previous mismatch where a static cfg.Max could diverge from the dynamic limit while preserving the existing sliding-window math and storage semantics.

@gaby gaby requested a review from Copilot November 23, 2025 15:36
@gaby
Copy link
Member Author

gaby commented Nov 23, 2025

/gemini review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request aims to fix the sliding window limiter to respect a dynamic MaxFunc value, recompute allowances for skipped requests, and adds a regression test. The refactoring improves code structure and robustness, for example by extracting logic into rotateWindow and ttlDuration functions. However, I've identified two significant issues in the new sliding window logic that could lead to incorrect rate limiting. One is a race condition in handling skipped requests, and the other is a bug in the window rotation logic when multiple window periods have passed. I've provided detailed comments and suggestions for fixes for both issues.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
middleware/limiter/limiter_test.go (1)

414-446: Align with existing test style: add t.Parallel() and ignore unused ctx in MaxFunc.

Functionally this test looks good and covers the dynamic MaxFunc vs static Max behavior and headers. For consistency with the rest of this file:

  • Call t.Parallel() at the top of the test.
  • Change the MaxFunc signature to ignore the unused fiber.Ctx parameter.

For example:

func Test_Limiter_Sliding_MaxFuncOverridesStaticMax(t *testing.T) {
-	app := fiber.New()
+	t.Parallel()
+	app := fiber.New()
@@
-	app.Use(New(Config{
-		Max:               staticMax,
-		MaxFunc:           func(fiber.Ctx) int { return dynamicMax },
+	app.Use(New(Config{
+		Max: staticMax,
+		MaxFunc: func(_ fiber.Ctx) int {
+			return dynamicMax
+		},
🧹 Nitpick comments (1)
middleware/limiter/limiter_sliding.go (1)

64-77: Sliding-window core logic looks correct; consider avoiding the second manager.get for efficiency.

The updated flow fixes the two main issues:

  • remaining is now derived from the dynamic maxRequests from MaxFunc, and headers use the same value.
  • In the skip path you re-rotate the window, optionally decrement currHits/prevHits, then recompute rate and remaining instead of doing a simple remaining++, which matches the sliding-window math.

One thing to consider:

  • After c.Next(), you reacquire the lock and call manager.get(reqCtx, key) again even though e is already a pointer to the entry you just persisted. For in‑process stores this is an extra map lookup; for remote stores it adds an extra I/O round-trip per request. You can likely reuse e directly in the second phase (still under mux.Lock()), just recomputing ts, resetInSec, weight, and then applying the skip logic before manager.set. That would preserve the behavior while reducing storage calls and keeping the per-request logic centered on a single in-memory entry.

Also applies to: 91-110, 118-120, 121-149

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8eabb4 and 035a918.

📒 Files selected for processing (2)
  • middleware/limiter/limiter_sliding.go (5 hunks)
  • middleware/limiter/limiter_test.go (3 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2024-06-30T00:38:06.580Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3051
File: middleware/session/session.go:215-216
Timestamp: 2024-06-30T00:38:06.580Z
Learning: Parallel tests for `Session.Save` already exist in the `middleware/session/session_test.go` file, specifically in the `Test_Session_Save` and `Test_Session_Save_Expiration` functions.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2025-10-16T07:19:52.418Z
Learnt from: grivera64
Repo: gofiber/fiber PR: 3807
File: adapter_test.go:118-144
Timestamp: 2025-10-16T07:19:52.418Z
Learning: In the Fiber codebase, the linter does not allow `require` assertions from within HTTP handlers (including net/http-style handlers). Use `t.Fatalf`, `t.Errorf`, or similar `testing.T` methods for error handling inside handler functions instead.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2025-05-07T13:07:33.899Z
Learnt from: mdelapenya
Repo: gofiber/fiber PR: 3434
File: docs/api/services.md:39-43
Timestamp: 2025-05-07T13:07:33.899Z
Learning: When documenting Go interface methods in the Fiber project, avoid showing method signatures with the interface type as the receiver (e.g., `func (d *Service) Method()`) since interfaces cannot be used as receivers in Go. Instead, show just the method signature without a receiver or use a placeholder implementation name.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-12-13T08:14:22.851Z
Learnt from: efectn
Repo: gofiber/fiber PR: 3162
File: hooks_test.go:228-228
Timestamp: 2024-12-13T08:14:22.851Z
Learning: In Go test files, prefer using the `require` methods from the `testify` package for assertions instead of manual comparisons and calls to `t.Fatal` or `t.Fatalf`.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-11-15T07:56:21.623Z
Learnt from: ReneWerner87
Repo: gofiber/fiber PR: 3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-08T19:06:06.583Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 2922
File: middleware/cors/utils.go:63-71
Timestamp: 2024-10-08T19:06:06.583Z
Learning: The project uses the testify/assert package for assertions in unit tests.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-02T23:03:31.727Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/store.go:164-167
Timestamp: 2024-10-02T23:03:31.727Z
Learning: Unit tests in this project use testify require.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-12T10:01:44.206Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/middleware_test.go:190-191
Timestamp: 2024-10-12T10:01:44.206Z
Learning: When testing session `IdleTimeout` expiration, it's acceptable to use `time.Sleep` to simulate the passage of time in tests.

Applied to files:

  • middleware/limiter/limiter_test.go
🧬 Code graph analysis (2)
middleware/limiter/limiter_sliding.go (1)
constants.go (1)
  • StatusBadRequest (73-73)
middleware/limiter/limiter_test.go (2)
middleware/limiter/config.go (1)
  • Config (10-75)
middleware/limiter/limiter_sliding.go (2)
  • SlidingWindow (15-15)
  • SlidingWindow (18-160)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: CodeQL analysis (go)
  • GitHub Check: Agent
  • GitHub Check: repeated
  • GitHub Check: Compare
🔇 Additional comments (2)
middleware/limiter/limiter_test.go (1)

772-801: LGTM: solid coverage of sliding-window recomputation after handler delay.

This test cleanly verifies that, with a delayed handler, the sliding window state and X-RateLimit-* headers are recalculated based on the updated window, and it follows existing test patterns (t.Parallel, plain handler, require in the test only). No changes needed.

middleware/limiter/limiter_sliding.go (1)

162-215: Overflow-safe TTL helpers are a nice improvement.

rotateWindow cleanly encapsulates the per-entry window rotation and returns resetInSec, and the new ttlDuration/secondsToDuration helpers correctly:

  • Convert second counts to time.Duration with a bound (maxSeconds) to avoid overflow.
  • Saturate to time.Duration(math.MaxInt64) both on conversion and on addition when resetDuration + expirationDuration would overflow.

This keeps the effective TTL as “time until reset + one more window” while making the arithmetic robust for large expirations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
middleware/limiter/limiter_test.go (1)

9-9: Dynamic MaxFunc test looks good; add t.Parallel() and ignore the unused ctx param

The new Test_Limiter_Sliding_MaxFuncOverridesStaticMax correctly asserts that MaxFunc overrides Max and that the headers track the dynamic max. Two small consistency/style nits:

  • Add t.Parallel() at the start of the test, as with most other tests in this file.
  • Change the MaxFunc signature to ignore the unused parameter explicitly:
-func Test_Limiter_Sliding_MaxFuncOverridesStaticMax(t *testing.T) {
-	app := fiber.New()
+func Test_Limiter_Sliding_MaxFuncOverridesStaticMax(t *testing.T) {
+	t.Parallel()
+	app := fiber.New()
@@
-	app.Use(New(Config{
-		Max:               staticMax,
-		MaxFunc:           func(fiber.Ctx) int { return dynamicMax },
+	app.Use(New(Config{
+		Max:               staticMax,
+		MaxFunc:           func(_ fiber.Ctx) int { return dynamicMax },

Also applies to: 440-472

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d7697a and a36f95e.

📒 Files selected for processing (2)
  • middleware/limiter/limiter_sliding.go (5 hunks)
  • middleware/limiter/limiter_test.go (4 hunks)
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2024-10-08T19:06:06.583Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/config.go:122-122
Timestamp: 2024-10-08T19:06:06.583Z
Learning: In `DefaultErrorHandler(c *fiber.Ctx, err error)`, since `c` is a pointer to an interface, we need to dereference `*c` when calling interface methods like `SendStatus`.

Applied to files:

  • middleware/limiter/limiter_sliding.go
📚 Learning: 2024-06-30T00:38:06.580Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3051
File: middleware/session/session.go:215-216
Timestamp: 2024-06-30T00:38:06.580Z
Learning: Parallel tests for `Session.Save` already exist in the `middleware/session/session_test.go` file, specifically in the `Test_Session_Save` and `Test_Session_Save_Expiration` functions.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2025-10-16T07:19:52.418Z
Learnt from: grivera64
Repo: gofiber/fiber PR: 3807
File: adapter_test.go:118-144
Timestamp: 2025-10-16T07:19:52.418Z
Learning: In the Fiber codebase, the linter does not allow `require` assertions from within HTTP handlers (including net/http-style handlers). Use `t.Fatalf`, `t.Errorf`, or similar `testing.T` methods for error handling inside handler functions instead.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2025-05-07T13:07:33.899Z
Learnt from: mdelapenya
Repo: gofiber/fiber PR: 3434
File: docs/api/services.md:39-43
Timestamp: 2025-05-07T13:07:33.899Z
Learning: When documenting Go interface methods in the Fiber project, avoid showing method signatures with the interface type as the receiver (e.g., `func (d *Service) Method()`) since interfaces cannot be used as receivers in Go. Instead, show just the method signature without a receiver or use a placeholder implementation name.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-12-13T08:14:22.851Z
Learnt from: efectn
Repo: gofiber/fiber PR: 3162
File: hooks_test.go:228-228
Timestamp: 2024-12-13T08:14:22.851Z
Learning: In Go test files, prefer using the `require` methods from the `testify` package for assertions instead of manual comparisons and calls to `t.Fatal` or `t.Fatalf`.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-11-15T07:56:21.623Z
Learnt from: ReneWerner87
Repo: gofiber/fiber PR: 3161
File: app.go:923-932
Timestamp: 2024-11-15T07:56:21.623Z
Learning: In the Fiber framework, breaking changes are acceptable when moving from version 2 to version 3, including modifications to method signatures such as in the `Test` method in `app.go`.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-08T19:06:06.583Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 2922
File: middleware/cors/utils.go:63-71
Timestamp: 2024-10-08T19:06:06.583Z
Learning: The project uses the testify/assert package for assertions in unit tests.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-02T23:03:31.727Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/store.go:164-167
Timestamp: 2024-10-02T23:03:31.727Z
Learning: Unit tests in this project use testify require.

Applied to files:

  • middleware/limiter/limiter_test.go
📚 Learning: 2024-10-12T10:01:44.206Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/middleware_test.go:190-191
Timestamp: 2024-10-12T10:01:44.206Z
Learning: When testing session `IdleTimeout` expiration, it's acceptable to use `time.Sleep` to simulate the passage of time in tests.

Applied to files:

  • middleware/limiter/limiter_test.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: unit (1.25.x, macos-latest)
  • GitHub Check: repeated
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: lint
  • GitHub Check: Compare
🔇 Additional comments (6)
middleware/limiter/limiter_test.go (2)

375-399: Nice focused regression test for disabled headers + skip path

TestLimiterSlidingSkipsPostUpdateWhenHeadersDisabled cleanly verifies that with DisableHeaders: true and no skip flags, the sliding limiter only does one get/set on storage. This directly guards against unnecessary post-handler storage work and matches the new if skipHit || !cfg.DisableHeaders condition.


798-827: Window rotation / expiration tests align well with sliding-window math

Both Test_Limiter_Sliding_Window_RecalculatesAfterHandlerDelay and Test_Limiter_Sliding_Window_ExpiresStalePrevHits do a good job of:

  • Exercising recomputation of rate/remaining after handler-induced delays, ensuring headers are based on the rotated window.
  • Verifying that prevHits are dropped after more than one full window, avoiding overly strict limiting.

The sleep margins look sufficient for second‑granularity timestamps, so these should be stable under normal CI load.

Also applies to: 829-853

middleware/limiter/limiter_sliding.go (4)

64-79: Core sliding-window rate/remaining computation now correctly uses dynamic maxRequests

The refactor to:

  • Rotate the window via rotateWindow(e, ts, expiration),
  • Compute rate := int(float64(e.prevHits)*weight) + e.currHits with weight derived from resetInSec/expiration, and
  • Derive remaining := maxRequests - rate using the per-request maxRequests from MaxFunc,

ensures dynamic limits are consistently applied for both enforcement and header values. Persisting with ttlDuration(resetInSec, expiration) also matches the “end of next window” TTL semantics described in the comment.

This looks logically sound and aligns with the new tests around dynamic MaxFunc and header behavior.

Also applies to: 92-96, 100-111


119-157: Skip‑hit path and window rotation fix the previous race and bucket mis-accounting

The combination of:

  • Recomputing ts, resetInSec, weight and re-running rotateWindow after c.Next(),
  • Using bucketForOriginalHit(e, windowExpiresAt, ts, expiration) to decide whether to decrement currHits or prevHits (or neither) for a skipped request, and
  • Recalculating rate/remaining before persisting and updating headers,

addresses the earlier issues where:

  • A long-running request could have its hit moved into the previous window, but the decrement still targeted currHits.
  • The remaining calculation after skip was previously adjusted by +1 instead of recomputing from the sliding-window formula.

The updated logic is guarded by if skipHit || !cfg.DisableHeaders, so you avoid extra storage churn when neither skipping nor emitting headers. Overall, this is a solid, race-resistant implementation of the skip behavior.

Also applies to: 163-197


199-225: TTL helpers correctly guard against duration overflow

ttlDuration and secondsToDuration provide a clear and safe way to derive:

  • A reset duration from resetInSec,
  • The base expiration window, and
  • A combined TTL clamped to math.MaxInt64 when either seconds are too large or resetDuration + expirationDuration would overflow.

This is a nice improvement over ad‑hoc arithmetic and should make behavior robust even with very large expiration values.


24-28: Config validation prevents the division-by-zero risk; however, the current approach lacks clarity about sub-second expiration handling

The concern in the review is partially mitigated by existing code. When cfg.Expiration is set to a sub-second value like 200 * time.Millisecond, the config validation in config.go (line 111) detects this because int(0.2) <= 0 evaluates to true, and automatically replaces it with the default 1 * time.Minute. This prevents the expiration == 0 scenario that would cause division by zero at line 72.

However, the review's underlying point remains valid: the behavior is implicit rather than explicit. The current code silently converts sub-second values without documentation, and the truncation via uint64(cfg.Expiration.Seconds()) is still a design issue even though it's protected by validation. Consider adding explicit validation with clear error messaging or documentation to make the sub-second limitation intentional and obvious to users.

@gaby
Copy link
Member Author

gaby commented Nov 23, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses the issue of the sliding window limiter not respecting the dynamic MaxFunc value. The changes correctly use the dynamic maxRequests for rate calculation and update the logic for skipped requests to re-compute allowance and refresh headers, which is a solid improvement. The refactoring of window rotation and TTL calculation logic into separate functions enhances code clarity and robustness, especially with the added overflow checks. The new tests, including the regression test for MaxFunc override and scenarios with handler delays, are comprehensive and ensure the correctness of the fixes. I've pointed out a minor correctness issue regarding floating-point truncation in the rate calculation which could make the limiter more permissive than intended. Overall, this is a high-quality contribution.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
middleware/limiter/limiter_sliding.go (1)

199-215: TTL helpers robustly guard against time.Duration overflow; consider a tiny constant for readability

ttlDuration + secondsToDuration correctly:

  • Convert seconds to time.Duration only when seconds <= math.MaxInt64 / int64(time.Second).
  • Clamp to time.Duration(math.MaxInt64) if either conversion would overflow or if resetDuration + expirationDuration would overflow.

This is a solid defensive improvement over ad-hoc TTL math. If you want a minor readability tweak, you could factor time.Duration(math.MaxInt64) into a small unexported const/var (e.g., maxDuration) to avoid repeating the literal and make the intent clearer, but that’s purely cosmetic.

Also applies to: 217-225

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a36f95e and 4299c50.

📒 Files selected for processing (1)
  • middleware/limiter/limiter_sliding.go (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-10-08T19:06:06.583Z
Learnt from: sixcolors
Repo: gofiber/fiber PR: 3016
File: middleware/session/config.go:122-122
Timestamp: 2024-10-08T19:06:06.583Z
Learning: In `DefaultErrorHandler(c *fiber.Ctx, err error)`, since `c` is a pointer to an interface, we need to dereference `*c` when calling interface methods like `SendStatus`.

Applied to files:

  • middleware/limiter/limiter_sliding.go
🧬 Code graph analysis (1)
middleware/limiter/limiter_sliding.go (2)
log/fiberlog.go (1)
  • Errorf (49-51)
constants.go (1)
  • StatusBadRequest (73-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: unit (1.25.x, windows-latest)
  • GitHub Check: unit (1.25.x, macos-latest)
  • GitHub Check: lint
  • GitHub Check: repeated
  • GitHub Check: Compare
🔇 Additional comments (4)
middleware/limiter/limiter_sliding.go (4)

5-8: Dynamic maxRequests and Ceil-based rate calculation look correct; verify Expiration can’t be zero

Using maxRequests := cfg.MaxFunc(c) consistently for remaining := maxRequests - rate and setting X-RateLimit-Limit fixes the static cfg.Max issue, and switching to math.Ceil(float64(e.prevHits)*weight) correctly avoids truncating fractional previous-window hits.

One assumption here is that expiration := uint64(cfg.Expiration.Seconds()) is always ≥ 1; if a caller configures Expiration < time.Second, expiration becomes 0 and weight := float64(resetInSec) / float64(expiration) will panic with divide-by-zero. If this isn’t already enforced in Config validation, consider guarding or documenting the minimum expiration.

Also applies to: 64-80, 100-110


119-156: Skip logic + post-handler recomputation correctly model sliding window and dynamic max

The skipHit predicate based on SkipSuccessfulRequests / SkipFailedRequests plus the guarded block if skipHit || !cfg.DisableHeaders is a good balance between correctness and overhead: you only re-read state when you need to adjust hits or refresh headers.

Recomputing ts, calling rotateWindow again, recomputing weight, and then rebuilding rate and remaining ensures the second pass uses a fresh view of the window (addressing the stale-weight concern) and includes concurrent hits that arrived while c.Next() ran. Using bucketForOriginalHit(e, windowExpiresAt, ts, expiration) to decide which bucket to decrement avoids the earlier race where a skipped request could accidentally remove another request’s hit after a window rotation.

Behaviorally this matches the objectives: dynamic maxRequests is honored, skipped hits are rolled back against the correct bucket, and X-RateLimit headers reflect the updated sliding-window state.


163-185: rotateWindow correctly handles single and multi-window expirations

The new rotateWindow logic:

  • Initializes e.exp on first use.
  • For ts >= e.exp, computes elapsed := ts - e.exp and:
    • Resets both prevHits and currHits when elapsed >= expiration, so hits older than a full window are fully discarded.
    • Otherwise shifts currHits into prevHits, zeros currHits, and sets e.exp = ts + expiration - elapsed, giving resetInSec = e.exp - ts = expiration - elapsed.

This fixes the earlier bug where prevHits could retain hits from windows that should have expired, and it keeps resetInSec consistent with the sliding-window math used for weight.


187-197: bucketForOriginalHit cleanly identifies the correct bucket for skipped hits

Using the captured windowExpiresAt (requestExpiration) to decide where the original hit landed works well:

  • ts < requestExpiration → still in the window that was “current” when the hit was added ⇒ decrement currHits.
  • ts-requestExpiration < expiration → now in the immediately previous window ⇒ decrement prevHits.
  • Otherwise, the hit’s window is older than one full expiration and is no longer counted ⇒ return nil.

The boundary choices (< vs <=) are appropriate: at ts == requestExpiration the hit moves into prevHits, and once ts-requestExpiration >= expiration the hit falls completely out of the sliding window. This gives the skip logic the necessary precision without over-complicating the state.

@gaby
Copy link
Member Author

gaby commented Nov 23, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request provides a comprehensive fix for several issues in the sliding window limiter. It correctly implements support for dynamic request limits using MaxFunc, which was a critical bug. The logic for window rotation, calculating request rates, and handling skipped requests has been significantly improved for correctness, especially in scenarios involving long-running requests or idle periods. Additionally, the introduction of overflow checks for TTL calculation makes the middleware more robust. The new tests are thorough and cover the fixed bugs and edge cases well. Overall, this is an excellent set of changes that greatly improves the reliability of the sliding window limiter.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

@gaby
Copy link
Member Author

gaby commented Nov 23, 2025

@codex review

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ReneWerner87
Copy link
Member

@gaby Can you mark the place in the PR where the problem was, which is why it was not reset?

@gaby gaby changed the title 🐛 bug: Fix sliding limiter to respect dynamic max 🐛 bug: Fix and improvements for the sliding window Limiter Nov 24, 2025
@ReneWerner87 ReneWerner87 merged commit 3a13562 into main Nov 24, 2025
19 of 22 checks passed
@ReneWerner87 ReneWerner87 deleted the update-rate-limit-to-use-dynamic-maxrequests branch November 24, 2025 12:07
@github-project-automation github-project-automation bot moved this to Done in v3 Nov 24, 2025
@coldsmirk
Copy link

👍 Excellent improvement! Are you planning to enhance it further, as mentioned in #3749 ?

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.

🐛 bug: Issues with limiter middleware

4 participants