Repository navigation
Remember unknown SDLC projects briefly; rate limit API requests per client - #17
Merged
andreasarf merged 4 commits intoNov 1, 2026
Merged
Conversation
…lient - external-projects-missing cache (30s): a project the SDLC system doesn't know is answered from the cache instead of asking again each time, and found soon after it is created there; a sync replaces it with the project. Used directly through the CacheManager, so failures are caught and logged like the cache error handler does - ClientRateLimitFilter on /api/* (FilterRegistrationBean, after the trace filter): requests per client IP across all instances (100/s, 429 + Retry-After); server.forward-headers-strategy=native takes the client IP from X-Forwarded-For only from internal proxy addresses - Tests: unknown answer remembered then found after expiry, sync clears it; per-client 429 with Retry-After and X-Trace-Id, separate per client, non-API paths unlimited; the repeated-lookup test counts calls after the first lookup, as the HTTP client may retry a GET on a stale connection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
…ases # Conflicts: # src/main/java/com/company/resourceapi/config/RateLimitProperties.java
…ed 429 - "Unknown project" is cached as Spring's null value in the external-projects cache with a per-entry TTL (30s unknown, 5m found), replacing the separate cache and the hand-written lookups; a sync simply @CachePut-replaces it. - RateLimiters stops asking Redis for 10s after a failure, so an outage doesn't add a timeout to every request. - Limiters expire 10 periods after last use (at least a minute) instead of an hour, so per-IP limiters don't pile up. - The per-client filter answers through the shared exception handler; refused callers are logged at DEBUG. - README: server.tomcat.remoteip.internal-proxies for the per-IP limit. - Tests: longer negative-cache TTL, no timing-sensitive 404 assertion, delta-based call counts, unit test for the outage pause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fourth Redis PR, stacked on #16 (rate limits). Merge in order #14 → #15 → #16 → this one. It adds the two use cases from the plan that #14–#16 didn't cover, plus hardening that the per-client limit made necessary.
Briefly cache "unknown project" answers
external-projectscache as Spring's cached null. A per-entry TTL keeps it for 30 s (resourceapi.cache.missing-external-projects-ttl); found projects are kept for 5 min. RepeatedGET /externalcalls for an unknown project no longer each call the SDLC system.@CachePut) replaces the cached answer right away.@Cacheableand@CachePut, no hand-written cache code, so the shared fail-soft cache error handler covers Redis outages.spring.cache.redis.cache-null-values=true(the default).Per-client API rate limit
ClientRateLimitFilterlimits each client IP on/api/*, across all instances: 100 per second (resourceapi.rate-limit.client.*), then 429 +Retry-After.RateLimitExceededException), like the other 429s, and is logged at DEBUG, so a flood of refusals doesn't flood the logs.FilterRegistrationBeanmaps it to/api/*only, so the OpenAPI document and Swagger UI aren't limited. It runs after the trace filter, so a refused request still carriesX-Trace-Id.server.forward-headers-strategy=nativemakes Tomcat take the client IP fromX-Forwarded-For, but only from internal proxy addresses. The README explains whenserver.tomcat.remoteip.internal-proxiesmust be set.Rate limiter hardening
These change
RateLimitersfrom #16.Native image fixes
Made in the PRs that introduced the code, then merged up:
Tests
CacheIntegrationTest:ClientRateLimitIntegrationTest:Retry-After+X-Trace-Id;/v3/api-docsisn't limited.RateLimitersTest(new): when Redis fails, requests are allowed and Redis is asked once, not on every request.RateLimitIntegrationTest: a limiter's Redis TTL is the 1-min minimum.Full suite: 112 tests pass, run twice.
Native image of this head, run against Redis 8 with a WireMock stand-in for the SDLC system:
🤖 Generated with Claude Code
https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2