Skip to content

Feature: Rate Limiting Filter - #83

Merged
simonforsberg merged 14 commits into
mainfrom
feature/40-rate-limiting-filter
Feb 18, 2026
Merged

Feature: Rate Limiting Filter#83
simonforsberg merged 14 commits into
mainfrom
feature/40-rate-limiting-filter

Conversation

@simonforsberg

@simonforsberg simonforsberg commented Feb 17, 2026

Copy link
Copy Markdown

Summary

Introduced RateLimitingFilter to protect the web server from request bursts and potential DoS attacks by limiting the number of requests per client IP address.

Changes

  • Added RateLimitingFilter: A new filter implementation using the Token Bucket algorithm (via Bucket4J) to enforce rate limits.
  • Configurable limits: Supports setting requests per minute and maximum burst capacity.
  • IP-based tracking: Uses ConcurrentHashMap to maintain independent buckets for each unique client IP.
  • Error handling: Returns a 429 Too Many Requests response with a Retry-After header when limits are exceeded.
  • Added RateLimitingFilterTest: Comprehensive unit tests covering:
    • Normal request flow within limits.
    • Blocking requests when limits are exceeded.
    • Independent tracking for different IP addresses.
    • Resource cleanup on filter destruction.
    • Validation of configuration parameters.

Technical Details

  • Dependency: Uses io.github.bucket4j:bucket4j-core for robust and thread-safe rate limiting logic.
  • Protocol Compliance: Implements the Retry-After header to inform clients when they can attempt to reconnect.
  • Logging: Integration with ServerLogging to provide visibility into rate-limited events for security monitoring.

Verification Results

  • All unit tests in RateLimitingFilterTest passed.
  • Verified that the filter correctly identifies and isolates traffic from different IP sources.

Summary by CodeRabbit

  • New Features

    • Per‑IP rate limiting: default 60 requests/minute with a burst of 10; excess requests receive 429 Too Many Requests.
  • Configuration

    • New rate‑limiting settings (enabled, requests‑per‑minute, burst‑capacity) with defaults.
  • Tests

    • Added unit tests for allow/block behavior, per‑IP isolation, lifecycle cleanup, and invalid config handling.
  • Chores

    • Added runtime dependency for rate limiting and helpers to read numeric config values.

simonforsberg and others added 7 commits February 13, 2026 14:31
Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
…gFilter using Bucket4j and add response handling for rate limit exceeded
…dation, and server cleanup. Add to App pipeline and configure properties.

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
…ng, rate limit enforcement, and cleanup behavior

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
…mprove test method naming, and add validation test

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
…and documentation

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
@simonforsberg simonforsberg linked an issue Feb 17, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown

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

Walkthrough

Adds a Bucket4J-based per-IP rate limiting filter, registers it in the global pipeline, introduces configuration and tests, extends ConfigLoader with rate-limiting fields/accessors, and adds the bucket4j Maven dependency.

Changes

Cohort / File(s) Summary
Dependencies
pom.xml
Added com.bucket4j:bucket4j_jdk17-core:8.16.1 dependency.
Filter Implementation
src/main/java/org/juv25d/filter/RateLimitingFilter.java
New public RateLimitingFilter implementing per‑IP token buckets: constructor validation, doFilter that responds 429 when limit exceeded, getTrackedIpCount(), and destroy().
Application Wiring
src/main/java/org/juv25d/App.java
Registers RateLimitingFilter in the global pipeline using config keys rate-limiting.requests-per-minute and rate-limiting.burst-capacity.
Configuration
src/main/resources/application-properties.yml
Added rate-limiting block: enabled: true, requests-per-minute: 60, burst-capacity: 10.
Config Loader
src/main/java/org/juv25d/util/ConfigLoader.java
Added requestsPerMinute and burstCapacity fields; loads rate-limiting config with defaults and exposes getRequestsPerMinute() and getBurstCapacity().
Tests
src/test/java/org/juv25d/filter/RateLimitingFilterTest.java
New JUnit5 + Mockito + AssertJ tests covering allow/block behavior, per‑IP isolation, destroy cleanup, and invalid constructor parameters.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Filter as RateLimitingFilter
    participant BucketStore as BucketStore
    participant Chain as FilterChain
    participant Server as Server

    Client->>Filter: HTTP request
    Filter->>Filter: extract client IP
    Filter->>BucketStore: get/create per-IP bucket
    Filter->>BucketStore: tryConsume(1)
    alt token available
        BucketStore-->>Filter: success
        Filter->>Chain: doFilter(request,response)
        Chain->>Server: handle request
        Server-->>Chain: response
        Chain-->>Filter: return
        Filter-->>Client: response
    else token not available
        BucketStore-->>Filter: failure
        Filter->>Filter: log warning
        Filter-->>Client: 429 Too Many Requests (Retry-After)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • Cavve
  • bamsemats

Poem

🐇 I count the hops, I count the beats,
Sixty per minute on busy streets.
Ten for bursts when numbers climb,
I guard the gate and mark the time.
Thump, nibble, log — steady server rhyme.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: a new Rate Limiting Filter feature is being added to the codebase.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/40-rate-limiting-filter

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.

@mattknatt mattknatt self-assigned this Feb 17, 2026
@simonforsberg simonforsberg self-assigned this Feb 17, 2026
@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown

Caution

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

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/ithsjava25/project-webserver-juv25d/issues/comments/3915739267","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: failure by coderabbit.ai -->\n\n> [!CAUTION]\n> ## Review failed\n> \n> Failed to post review comments\n\n<!-- end of auto-generated comment: failure by coderabbit.ai -->\n\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nThe PR introduces rate-limiting functionality to the application using the Bucket4J library. A new RateLimitingFilter implements per-IP request throttling at 60 requests/minute with a burst capacity of 10, integrated into the application's filter chain with corresponding configuration.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Dependencies** <br> `pom.xml`|Added bucket4j_jdk17-core version 8.16.1 dependency for rate-limiting implementation.|\n|**Filter Implementation** <br> `src/main/java/org/juv25d/filter/RateLimitingFilter.java`|New RateLimitingFilter class implementing per-IP rate limiting using token bucket algorithm; tracks concurrent IPs, validates configuration, and responds with 429 status when limits exceeded.|\n|**Application Wiring** <br> `src/main/java/org/juv25d/App.java`|Registers RateLimitingFilter as global pipeline filter with parameters (60, 10) to enable rate limiting on all requests.|\n|**Configuration** <br> `src/main/resources/application-properties.yml`|Added rate-limiting configuration block specifying enabled status, requests-per-minute (60), and burst-capacity (10).|\n|**Test Suite** <br> `src/test/java/org/juv25d/filter/RateLimitingFilterTest.java`|Comprehensive unit tests covering rate limit enforcement, per-IP independence, bucket cleanup, and invalid configuration handling.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant RateLimitingFilter\n    participant TokenBucket\n    participant FilterChain\n    participant Server\n\n    Client->>RateLimitingFilter: HTTP Request\n    RateLimitingFilter->>RateLimitingFilter: Extract client IP\n    RateLimitingFilter->>TokenBucket: Get/Create per-IP bucket\n    RateLimitingFilter->>TokenBucket: tryConsume(1 token)\n    \n    alt Token Available\n        TokenBucket-->>RateLimitingFilter: success\n        RateLimitingFilter->>FilterChain: doFilter(request, response)\n        FilterChain->>Server: Process request\n        Server-->>FilterChain: Response\n        FilterChain-->>RateLimitingFilter: Complete\n        RateLimitingFilter-->>Client: Response\n    else Rate Limit Exceeded\n        TokenBucket-->>RateLimitingFilter: failure\n        RateLimitingFilter->>RateLimitingFilter: Log warning\n        RateLimitingFilter-->>Client: 429 Too Many Requests<br/>(with Retry-After header)\n    end\n```\n\n## Estimated code review effort\n\n🎯 3 (Moderate) | ⏱️ ~25 minutes\n\n## Possibly related PRs\n\n- ithsjava25/project-webserver-juv25d#59: Both PRs modify `App.java` to register IP-focused filters and introduce per-IP request handling logic into the application pipeline.\n\n## Suggested reviewers\n\n- Cavve\n- annikaholmqvist94\n- bamsemats\n\n## Poem\n\n> 🐰 A bunny hops with tokens in paw,  \n> Rate-limiting requests by IP law,  \n> Sixty per minute, ten to burst bright,  \n> The filter bounds traffic just right! 🪣✨\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 3</summary>\n\n<details>\n<summary>✅ Passed checks (3 passed)</summary>\n\n|     Check name     | Status   | Explanation                                                                                                                                                                          |\n| :----------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                          |\n|     Title check    | ✅ Passed | The title 'Feature: Rate limiting filter' directly and clearly describes the main addition in the changeset: a new rate-limiting filter using the Token Bucket algorithm (Bucket4J). |\n| Docstring Coverage | ✅ Passed | Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.                                                                                                  |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feature/40-rate-limiting-filter`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- announcements_start -->\n\n> [!TIP]\n> [Issue Planner](https://www.coderabbit.ai/issue-planner) is now in beta. Read the [docs](https://docs.coderabbit.ai/issues/planning) and try it out! Share your feedback on [Discord](https://discord.com/invite/coderabbit).\n\n<!-- announcements_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ithsjava25/project-webserver-juv25d&utm_content=83)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/ithsjava25/project-webserver-juv25d/issues/comments/3915739267","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Tue, 17 Feb 2026 16:32:02 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"381F:9B0CE:41768C:11A6FCA:69949801","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

@simonforsberg simonforsberg changed the title Feature: Rate limiting filter Feature: Rate Limiting Filter Feb 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/main/java/org/juv25d/App.java (2)

36-37: Three independent StaticFilesPlugin instances are created for what appears to be the same plugin.

Lines 32, 36, and 37 each call new StaticFilesPlugin(). If the plugin is stateless (as it typically would be), a single shared instance avoids the overhead of multiple objects and makes the intent clearer.

♻️ Suggested refactor
+StaticFilesPlugin staticFilesPlugin = new StaticFilesPlugin();
-pipeline.setPlugin(new StaticFilesPlugin());   // fix the method name first
+pipeline.setPlugin(staticFilesPlugin);
 ...
-router.registerPlugin("/", new StaticFilesPlugin());
-router.registerPlugin("/*", new StaticFilesPlugin());
+router.registerPlugin("/", staticFilesPlugin);
+router.registerPlugin("/*", staticFilesPlugin);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/App.java` around lines 36 - 37, Multiple new
StaticFilesPlugin() instances are created for the same plugin; instead
instantiate a single StaticFilesPlugin and reuse it when calling
router.registerPlugin for all routes. Locate the three registerPlugin calls that
pass new StaticFilesPlugin() (the calls to router.registerPlugin for the root
and wildcard paths), create one shared variable (e.g., staticFilesPlugin)
assigned to new StaticFilesPlugin(), and replace each new StaticFilesPlugin()
argument with that variable so the same instance is registered everywhere.

31-31: Unbounded ConcurrentHashMap in RateLimitingFilter will leak memory over time.

As seen in RateLimitingFilter.java (lines 24–25), a new Bucket is created and retained for every unique client IP, and is never evicted — only destroy() clears the map. On a long-running public-facing server this accumulates one entry per unique IP indefinitely and can exhaust heap memory.

Consider switching to a bounded cache (e.g., Caffeine or Guava CacheBuilder) with a TTL/expiry, or periodically pruning stale entries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/App.java` at line 31, The RateLimitingFilter
currently stores per-IP Bucket instances in an unbounded ConcurrentHashMap
causing memory leaks; change its storage to a bounded expiring cache (e.g.,
Caffeine or Guava Cache) with a maximumSize and
expireAfterAccess/expireAfterWrite policy, update constructor/newBucket lookup
to use Cache.get(key, ...) or Cache.getIfPresent/computeIfAbsent equivalent, and
modify destroy() to call cache.invalidateAll()/cache.cleanUp() instead of
clearing the map; ensure all references to the old ConcurrentHashMap (the map
that holds Bucket) are replaced and tests still create/remove buckets via the
cache API so stale IPs are evicted automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/juv25d/App.java`:
- Line 32: Remove the erroneous pipeline.setPlugin(new StaticFilesPlugin())
call: Pipeline has no setPlugin method so delete that line and instead register
the StaticFilesPlugin with the SimpleRouter instance used in this file (where
other plugins are being configured), then keep setting the configured router on
the pipeline (pipeline.setRouter(router) / similar) so the pipeline uses the
router with the plugin registered.
- Line 31: Replace the hardcoded rate-limiter magic numbers in the App startup
with values from the configuration: read the limit and burst (or equivalent
names) via ConfigLoader (or its accessor method) and pass those variables into
the RateLimitingFilter constructor instead of (60, 10); update the call site
pipeline.addGlobalFilter(new RateLimitingFilter(...), 0) to use
ConfigLoader.getXxx() (or the existing config object) so operators can tune
limits via application-properties.yml.

---

Nitpick comments:
In `@src/main/java/org/juv25d/App.java`:
- Around line 36-37: Multiple new StaticFilesPlugin() instances are created for
the same plugin; instead instantiate a single StaticFilesPlugin and reuse it
when calling router.registerPlugin for all routes. Locate the three
registerPlugin calls that pass new StaticFilesPlugin() (the calls to
router.registerPlugin for the root and wildcard paths), create one shared
variable (e.g., staticFilesPlugin) assigned to new StaticFilesPlugin(), and
replace each new StaticFilesPlugin() argument with that variable so the same
instance is registered everywhere.
- Line 31: The RateLimitingFilter currently stores per-IP Bucket instances in an
unbounded ConcurrentHashMap causing memory leaks; change its storage to a
bounded expiring cache (e.g., Caffeine or Guava Cache) with a maximumSize and
expireAfterAccess/expireAfterWrite policy, update constructor/newBucket lookup
to use Cache.get(key, ...) or Cache.getIfPresent/computeIfAbsent equivalent, and
modify destroy() to call cache.invalidateAll()/cache.cleanUp() instead of
clearing the map; ensure all references to the old ConcurrentHashMap (the map
that holds Bucket) are replaced and tests still create/remove buckets via the
cache API so stale IPs are evicted automatically.

Comment thread src/main/java/org/juv25d/App.java Outdated
Comment thread src/main/java/org/juv25d/App.java Outdated
Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/main/java/org/juv25d/App.java (1)

26-31: Execution order for equal-priority filters is deterministic, not undefined.

FilterRegistration.compareTo() compares only the order field using Integer.compare(). When filters share priority 0, the method returns 0, and Java's stable sort (used by Stream.sorted()) preserves their insertion order. The three filters will reliably execute in the order they were added: IpFilter, LoggingFilter, then RateLimitingFilter.

That said, if you prefer rate limiting to run before logging to avoid unnecessary token-bucket consumption and incorrect 429 responses for already-blocked IPs, assigning distinct priorities (e.g., IpFilter: 1, RateLimitingFilter: 2, LoggingFilter: 3) is a reasonable architectural choice—but it's not required for ordering reliability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/App.java` around lines 26 - 31, The comment points
out that equal orders are deterministic due to FilterRegistration.compareTo()
using Integer.compare(), so the current insertion order
(pipeline.addGlobalFilter with IpFilter, LoggingFilter, RateLimitingFilter) will
run in that sequence; if you want RateLimitingFilter to run before LoggingFilter
(to avoid logging or token use for blocked IPs), assign distinct priority values
when registering the filters (e.g., give IpFilter=1, RateLimitingFilter=2,
LoggingFilter=3) or otherwise change the integer order passed to
pipeline.addGlobalFilter; locate FilterRegistration.compareTo, and the three
registrations (IpFilter, LoggingFilter, RateLimitingFilter) in App.java to
update the order integers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/org/juv25d/App.java`:
- Line 31: The hardcoded RateLimitingFilter parameters in App.java
(pipeline.addGlobalFilter(new RateLimitingFilter(60, 10), 0)) should be replaced
with values read from your configuration loader: call the ConfigLoader (e.g.,
ConfigLoader.getRateLimitingRequestsPerMinute() and
ConfigLoader.getRateLimitingBurstCapacity() or equivalent methods) to obtain
requests-per-minute and burst-capacity, validate/convert them to the expected
types, and pass those values into new RateLimitingFilter; keep sensible defaults
if config values are missing and update any unit tests or usages that assumed
the hardcoded values.

---

Nitpick comments:
In `@src/main/java/org/juv25d/App.java`:
- Around line 26-31: The comment points out that equal orders are deterministic
due to FilterRegistration.compareTo() using Integer.compare(), so the current
insertion order (pipeline.addGlobalFilter with IpFilter, LoggingFilter,
RateLimitingFilter) will run in that sequence; if you want RateLimitingFilter to
run before LoggingFilter (to avoid logging or token use for blocked IPs), assign
distinct priority values when registering the filters (e.g., give IpFilter=1,
RateLimitingFilter=2, LoggingFilter=3) or otherwise change the integer order
passed to pipeline.addGlobalFilter; locate FilterRegistration.compareTo, and the
three registrations (IpFilter, LoggingFilter, RateLimitingFilter) in App.java to
update the order integers.

…ndling in tests

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/test/java/org/juv25d/filter/RateLimitingFilterTest.java (2)

120-129: Consider also asserting that negative constructor arguments are rejected.

Only 0 is tested for each parameter; negative values (-1, etc.) are equally invalid for a capacity/rate but are currently untested. If the validation guard is > 0, negatives are already covered — but the tests don't confirm this.

✅ Suggested additional assertions
     assertThatThrownBy(() -> new RateLimitingFilter(60, 0))
         .isInstanceOf(IllegalArgumentException.class);
+
+    assertThatThrownBy(() -> new RateLimitingFilter(-1, 5))
+        .isInstanceOf(IllegalArgumentException.class);
+
+    assertThatThrownBy(() -> new RateLimitingFilter(60, -1))
+        .isInstanceOf(IllegalArgumentException.class);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/filter/RateLimitingFilterTest.java` around lines 120
- 129, Add assertions to RateLimitingFilterTest that verify negative constructor
arguments are rejected: call new RateLimitingFilter(-1, 5) and new
RateLimitingFilter(60, -1) (and optionally both negative) inside
assertThatThrownBy and assert IllegalArgumentException. This ensures the
constructor validation in RateLimitingFilter for both capacity/rate parameters
(the RateLimitingFilter(...) constructor) rejects values less than zero as well
as zero.

85-85: Nit: misleading comment — "Empty first bucket" should be "Exhaust first bucket".

"Empty" implies clearing the IP map; the intent is to consume all tokens in the bucket for IP1.

📝 Suggested fix
-        for (int i = 0; i < 6; i++) { // Empty first bucket
+        for (int i = 0; i < 6; i++) { // Exhaust first bucket
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/filter/RateLimitingFilterTest.java` at line 85,
Update the misleading inline comment in the test loop inside
RateLimitingFilterTest (the for-loop at the top of the test method that iterates
6 times to consume tokens for IP1) from "Empty first bucket" to "Exhaust first
bucket" so it accurately reflects that the loop is consuming all tokens rather
than clearing the IP map; locate the loop in the RateLimitingFilterTest class
and replace the comment text only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/juv25d/util/ConfigLoader.java`:
- Around line 65-67: The getLong(String) method is incorrect and should be
removed; instead extend ConfigLoader.loadConfiguration() to parse the
"rate-limiting" section and populate new instance fields (e.g., long
requestsPerMinute, long burstCapacity), then add typed accessors
getRequestsPerMinute() and getBurstCapacity() used by App.java (which currently
calls getLong with keys); ensure loadConfiguration reads
"rate-limiting.requests-per-minute" and "rate-limiting.burst-capacity" from the
source config, converts to long safely, assigns to the new fields, and replace
any calls to getLong(...) with the new getters.

In `@src/test/java/org/juv25d/filter/RateLimitingFilterTest.java`:
- Around line 64-69: The Retry-After header is currently hardcoded to "60" in
RateLimitingFilter (around the token-exhaustion logic) but should use the bucket
probe to compute the actual refill wait; replace the hardcoded value with the
bucket4j computation probe.estimateAbilityToConsume(1).getRoundedSecondsToWait()
when setting the "Retry-After" response header in RateLimitingFilter, and update
the test in RateLimitingFilterTest to assert the header equals that computed
value (or compute the same probe value in the test before verifying) instead of
expecting "60".

---

Nitpick comments:
In `@src/test/java/org/juv25d/filter/RateLimitingFilterTest.java`:
- Around line 120-129: Add assertions to RateLimitingFilterTest that verify
negative constructor arguments are rejected: call new RateLimitingFilter(-1, 5)
and new RateLimitingFilter(60, -1) (and optionally both negative) inside
assertThatThrownBy and assert IllegalArgumentException. This ensures the
constructor validation in RateLimitingFilter for both capacity/rate parameters
(the RateLimitingFilter(...) constructor) rejects values less than zero as well
as zero.
- Line 85: Update the misleading inline comment in the test loop inside
RateLimitingFilterTest (the for-loop at the top of the test method that iterates
6 times to consume tokens for IP1) from "Empty first bucket" to "Exhaust first
bucket" so it accurately reflects that the loop is consuming all tokens rather
than clearing the IP map; locate the loop in the RateLimitingFilterTest class
and replace the comment text only.

Comment thread src/main/java/org/juv25d/util/ConfigLoader.java Outdated
Comment thread src/test/java/org/juv25d/filter/RateLimitingFilterTest.java
simonforsberg and others added 3 commits February 18, 2026 09:15
…ne to use dynamic values

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
pom.xml (1)

47-47: Redundant <scope>compile</scope>

compile is Maven's default scope; the explicit declaration adds noise without effect.

♻️ Proposed cleanup
         <dependency>
             <groupId>com.bucket4j</groupId>
             <artifactId>bucket4j_jdk17-core</artifactId>
             <version>8.16.1</version>
-            <scope>compile</scope>
         </dependency>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` at line 47, Remove the redundant <scope>compile</scope> elements
from dependency declarations in the POM (they're the default and add noise);
locate occurrences of the <scope>compile</scope> tag in the pom.xml (within
<dependency> blocks) and delete those lines so dependencies rely on the implicit
compile scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/juv25d/util/ConfigLoader.java`:
- Around line 13-14: The fields requestsPerMinute and burstCapacity in class
ConfigLoader are left at Java default 0L when the rate-limiting YAML section is
absent, causing RateLimitingFilter to throw; initialize these fields to the same
default constants/values used in the getOrDefault calls (the defaults currently
expected for rate limiting, e.g. 300 and 75) so that requestsPerMinute and
burstCapacity are non-zero even if the rateLimitingConfig block is skipped;
update the field declarations for requestsPerMinute and burstCapacity in
ConfigLoader (and any related default handling around where getOrDefault is
used) to use those default values.
- Around line 51-55: Add support for the rate-limiting "enabled" flag: update
ConfigLoader to read the "enabled" boolean from the rate-limiting map (e.g., set
a new private boolean field like rateLimitingEnabled when parsing the
"rate-limiting" block where requestsPerMinute and burstCapacity are read) and
expose it via a public isRateLimitingEnabled() getter; then change App (where
the filter is registered) to guard the RateLimitingFilter registration with
config.isRateLimitingEnabled() so the filter is only added when the flag is
true.

---

Nitpick comments:
In `@pom.xml`:
- Line 47: Remove the redundant <scope>compile</scope> elements from dependency
declarations in the POM (they're the default and add noise); locate occurrences
of the <scope>compile</scope> tag in the pom.xml (within <dependency> blocks)
and delete those lines so dependencies rely on the implicit compile scope.

Comment thread src/main/java/org/juv25d/util/ConfigLoader.java
Comment thread src/main/java/org/juv25d/util/ConfigLoader.java
johanbriger
johanbriger previously approved these changes Feb 18, 2026

@johanbriger johanbriger left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice work!
The Token Bucket implementation via Bucket4j is clean and robust solution.

… and ConfigLoader

Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
@TatjanaTrajkovic
TatjanaTrajkovic self-requested a review February 18, 2026 09:49

@TatjanaTrajkovic TatjanaTrajkovic left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Great implementation.

@simonforsberg
simonforsberg merged commit 97381b0 into main Feb 18, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 26, 2026
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.

Rate Limiting Filter

4 participants