feat(api): flexible and configurable rate limiting (#289) - #7983
antoinemzs wants to merge 17 commits into
Conversation
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
|
📖 Documentation check — ✅ Passed 25 functional file(s), 1 doc file(s) changed. Documentation-worthy changes detected and documentation was updated. 👏 Detected changes (covered by doc updates)
|
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
|
Rate limiting is throttling the E2E tests. |
There was a problem hiding this comment.
Pull request overview
Adds configurable API rate limiting with Bucket4j, security-filter integration, endpoint annotations, tests, and documentation.
Changes:
- Adds in-memory token-bucket storage and rate-limit configuration.
- Applies unauthenticated filtering and annotated endpoint throttling.
- Adds REST error handling, tests, dependency updates, and documentation.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Summary and recorded findings |
|---|---|
openaev-api/src/test/resources/application.properties |
Disables rate limiting by default in tests. |
openaev-api/src/test/java/io/openaev/utils/mockConfig/WithMockRateLimitConfig.java |
Provides test rate-limit configuration. |
openaev-api/src/test/java/io/openaev/ratelimit/support/ThrottledEndpoint.java |
Defines throttled test endpoints. |
openaev-api/src/test/java/io/openaev/ratelimit/RateLimitIntegrationTest.java |
Tests authenticated and unauthenticated throttling. |
openaev-api/src/main/java/io/openaev/rest/helper/RestBehavior.java |
Maps rate-limit exceptions to HTTP 429 responses. |
openaev-api/src/main/java/io/openaev/ratelimit/store/StoreProvider.java |
Lazily initializes the configured store. |
openaev-api/src/main/java/io/openaev/ratelimit/store/StoreFactory.java |
Creates store backends. Moderate (1 vote): the only backend is process-local, so quotas scale with replica count. |
openaev-api/src/main/java/io/openaev/ratelimit/store/Store.java |
Defines the storage contract. |
openaev-api/src/main/java/io/openaev/ratelimit/store/request/LimitSpecification.java |
Defines rate-limit parameters. |
openaev-api/src/main/java/io/openaev/ratelimit/store/request/LimitConsumptionRequest.java |
Defines bucket lookup requests. Critical (3 votes): nullable anonymous principals do not match existing keys, bypassing annotated quotas. |
openaev-api/src/main/java/io/openaev/ratelimit/store/LimitType.java |
Defines supported limit types. |
openaev-api/src/main/java/io/openaev/ratelimit/store/LimitFactory.java |
Defines limit factory behavior. |
openaev-api/src/main/java/io/openaev/ratelimit/store/Limit.java |
Defines limit results. |
openaev-api/src/main/java/io/openaev/ratelimit/store/impl/Limit.java |
Implements limit results. |
openaev-api/src/main/java/io/openaev/ratelimit/store/impl/InMemoryBucketStore.java |
Implements in-memory Bucket4j storage. Moderate (3 votes): reset nanoseconds are exposed as seconds. Moderate (3 votes): buckets are retained indefinitely without eviction or expiry. |
openaev-api/src/main/java/io/openaev/ratelimit/store/impl/BucketFactory.java |
Creates token buckets. |
openaev-api/src/main/java/io/openaev/ratelimit/service/RateLimitService.java |
Coordinates consumption. Moderate (3 votes): service-wide and store-wide monitors serialize all requests. |
openaev-api/src/main/java/io/openaev/ratelimit/model/RateLimitedPrincipal.java |
Represents rate-limit identities. |
openaev-api/src/main/java/io/openaev/ratelimit/filter/PreliminaryRateLimitFilter.java |
Applies preliminary unauthenticated limits. Critical (3 votes): getLocalAddr() buckets the API host rather than the client. Critical (1 vote): authenticated requests bypass the rate-limit service. |
openaev-api/src/main/java/io/openaev/ratelimit/exception/RateLimitedException.java |
Defines throttling exceptions. |
openaev-api/src/main/java/io/openaev/ratelimit/config/RateLimitStoreBackendValues.java |
Defines backend property values. |
openaev-api/src/main/java/io/openaev/ratelimit/config/RateLimitStoreBackend.java |
Defines backend options. |
openaev-api/src/main/java/io/openaev/ratelimit/config/RateLimitConfig.java |
Defines rate-limit settings. Critical (1 vote): the default in-memory value does not match IN_MEMORY and can prevent startup. Moderate (1 vote): authenticatedRps is never read. |
openaev-api/src/main/java/io/openaev/ratelimit/config/Limits.java |
Defines default limits and refill timing. |
openaev-api/src/main/java/io/openaev/ratelimit/aop/RateLimitAspect.java |
Applies method-level limits. Critical (1 vote): only the test controller is annotated, leaving production endpoints unprotected. Moderate (3 votes): each annotated request performs a database lookup for the current user. |
openaev-api/src/main/java/io/openaev/ratelimit/aop/RateLimit.java |
Defines endpoint annotations. Moderate (1 vote): the configured authenticated quota is not consulted. |
openaev-api/src/main/java/io/openaev/config/AppSecurityConfig.java |
Registers the rate-limit filter. |
openaev-api/pom.xml |
Adds the Bucket4j dependency. |
docs/docs/usage/rest-api.md |
Documents REST behavior. Moderate (2 votes): the documented cookie name differs from the implementation at lines 34 and 205. |
Suppressed comments (5)
docs/docs/usage/rest-api.md:205
- issue (blocking): This new user-facing section is left as
TODOand does not document theopenaev.ratelimit.*properties, defaults, per-endpoint@RateLimitbehavior, response headers, or the fact that the in-memory backend is process-local. Operators cannot configure or correctly size the feature from the published REST API documentation; replace the placeholder with the actual configuration and deployment semantics.
The API is globally rate limited (TODO: complete this section)
openaev-api/src/main/java/io/openaev/ratelimit/aop/RateLimit.java:12
- issue (blocking): This annotation default is the only place the configured authenticated quota could be selected, but it is a compile-time constant and
authenticatedRpsis never consulted; the preliminary filter skips authenticated requests. The advertisedopenaev.ratelimit.authenticated-rpssetting therefore has no effect. Wire the bound setting into the authenticated policy and apply it to the intended endpoints.
long rps() default Limits.AUTHENTICATED_RPS;
openaev-api/src/main/java/io/openaev/ratelimit/config/RateLimitConfig.java:24
- issue (blocking): These two
@Valueexpressions resolvedefault-rpsandauthenticated-rpsat the root of the environment, not underopenaev.ratelimit. As a result,openaev.ratelimit.default-rpsandopenaev.ratelimit.authenticated-rpscannot override the defaults, so the advertised limits are not configurable through the feature's namespace. Use the prefixed keys or bind these fields solely through@ConfigurationProperties.
@Value("${default-rps:" + Limits.DEFAULT_RPS + "}")
openaev-api/src/main/java/io/openaev/ratelimit/config/RateLimitConfig.java:28
- issue (blocking):
authenticatedRpsis never read: annotated methods userateLimit.rps(), whose default is the compile-timeLimits.AUTHENTICATED_RPS. Changingopenaev.ratelimit.authenticated-rpstherefore has no effect, so the authenticated allowance is not configurable as described. Resolve the configured default in the aspect or remove this unused setting.
@Value("${authenticated-rps:" + Limits.AUTHENTICATED_RPS + "}")
openaev-api/src/main/java/io/openaev/ratelimit/store/StoreFactory.java:17
- issue (blocking): The only implemented backend is a process-local
InMemoryBucketStore. With multiple API instances, each JVM gets an independent quota, so the effective allowance scales with the replica count even though the documentation calls the API globally rate limited. Provide a shared backend for clustered deployments or explicitly document and configure this as per-instance limiting.
case IN_MEMORY -> new InMemoryBucketStore(new BucketFactory(rateLimitConfig));
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Signed-off-by: Antoine MAZEAS <antoine.mazeas@filigran.io>
Proposed changes
Testing Instructions
Related issues
Checklist
Further comments
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...