Repository navigation
feat: rate-limit guest ticket creation and replies per IP - #96
Merged
Merged
Conversation
escalated.guest-rate-limit.{enabled,tickets-per-minute,replies-per-minute}
(defaults true/5/10) and a GuestRateLimitStore with an in-memory fixed-window
default. A host bean of that type replaces it for multi-instance deployments.
A HandlerInterceptor enforces @GuestThrottle before argument resolution, so wrong-token replies and unreadable bodies are counted. Over the limit the request gets 429 with Retry-After. The widget and guest-access reply routes share the reply counter. Mirrors escalated-dev/escalated-nestjs#130.
Member
Author
|
Once #97 lands, unknown guest tokens get a real 403 (matching the NestJS reference) instead of an unmapped EntityNotFoundException. At that point, |
Merged
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.
Port of escalated-dev/escalated-nestjs#130 (reference).
Behaviour
escalated.guest-rate-limit.enabled(defaulttrue),.tickets-per-minute(default 5),.replies-per-minute(default 10).POST /escalated/api/widget/ticketsand the guest reply endpoints (POST /escalated/api/widget/tickets/{token}/repliesandPOST /escalated/api/guest/tickets/{token}/replies) are limited per client IP over a 60s window. Tickets and replies have separate counters; the two reply routes share the reply counter.HandlerInterceptoracting on@GuestThrottlehandlers. It runs before argument resolution and before the handler looks up the guest token, so wrong-token replies (and unreadable bodies) count.429withRetry-Afterand{"error":"Too many requests. Please try again later."}.InMemoryGuestRateLimitStore, expired windows swept once a minute). No new dependency.GuestRateLimitStorebean (e.g. Redis-backed), the interceptor uses it instead.server.forward-headers-strategyand trust your proxies, or every guest shares one IP.Differences from the reference
EntityNotFoundExceptionfromTicketService.findByGuestToken, which no handler maps to a status (a separate, pre-existing gap). The wrong-token test stubs the service to answer 404, so it expects404, 404, 429rather than403, 403, 429.Retry-Afteris the time until the window closes.Tests
Written first; 6 of the 10
GuestThrottleTestcases failed before the interceptor existed (no 429 returned).GuestThrottleTest(MockMvc over the widget + guest controllers): 6th ticket -> 429 (service called 5 times);Retry-Afterin 1..60; 11th reply -> 429; wrong-token replies count (limit 2: 404, 404, 429);/guestreplies share the reply counter; tickets and replies separate; IPs separate; configured limit honoured (2: 201, 201, 429); disabled -> never 429; defaults 5/10/enabled.GuestThrottleInterceptorTest: a host-definedGuestRateLimitStoregets theescalated:guest:reply:<ip>key with a 60s window; the in-memory window resets once it closes.SecurityWiringTest$GuestEndpoints: in a booted host with the real security chain, the limit is live (limit 1: 400, 429).Results
./gradlew test(H2): 376 tests, 0 failures, 0 errors, 9 skipped (skips are pre-existing)./gradlew checkstyleMain checkstyleTest: BUILD SUCCESSFUL; no warnings in any file this PR touches (the existing warnings are elsewhere)