Repository navigation
fix(rate-limit): scope limiter key per route and return a real 429 body - #639
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-639/ This page is automatically updated on each push to this PR. |
There was a problem hiding this comment.
🟢 Approval recommended
The focused implementation addresses both root causes and includes appropriate regression coverage.
0 open findings
What changed in this PR
Scopes rate-limit counters per HTTP method and route while preserving actionable 429 responses.
Changes:
- Adds route-specific limiter signatures.
- Preserves 429 messages and retry headers.
- Adds regression tests for limiter isolation and responses.
| File | Description |
|---|---|
app/Http/Middleware/RateLimitMiddleware.php |
Scopes limiter keys by route and method. |
app/Exceptions/Handler.php |
Returns proper JSON 429 responses and headers. |
tests/Unit/RateLimitMiddlewareTest.php |
Tests isolation, enforcement, and rendering. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ref: https://app.clickup.com/t/86bchtzb4
Summary
Promo code
GET /api/v1/summits/{id}/promo-codes/{code}/applywas returning{"message":"server error"}(HTTP 429) to every user behind an IP that had previously used anotherrate.limitroute. During 2026 OCP Global (summit 73) the venue IP stayed blocked on checkout for about an hour after a show-adminPUT .../submission-period/reopen/notify.Root cause
RateLimitMiddlewarekeyed its counter onsha1(domain|ip)only (request()->user()is always null for OAuth2 bearer requests). Everyrate.limitroute, and the wholepublic_apigroup, shared one counter and one window per IP. The first writer of a key fixes its TTL, so notify's 30-hit / 3600 s window became the budget of apply (25,1). Verified in prod Redis: the key held 47 hits with the TTL created at the exact second of a notify call.Handler::renderturned everyHttpExceptioninto{"message":"server error"}and dropped the exception headers, so a 429 looked like a server fault and carried noRetry-After.Changes
RateLimitMiddleware: overrideresolveRequestSignatureso the key includes the HTTP method and the matched route template (not the concrete URL). Each rate limited route now has its own counter and window per IP.Handler:TooManyRequestsHttpException(whichThrottleRequestsExceptionextends) is rendered as{"message":"Too Many Requests"}with status 429 and the exception headers (Retry-After,X-RateLimit-*). Every other exception keeps the current behaviour.Deliberately not changed: units and numeric limits. The second
rate.limitargument is seconds by convention in this codebase (see the note on the notify route and #590), so25,1and10000,1are per second today. Enforcing the per-minute intent during a live event could create new 429s for shared venue IPs, so the unit/limit audit goes in a separate ticket.Deployment notes
Tests
tests/Unit/RateLimitMiddlewareTest.php:ThrottleRequestsException);Too Many RequestswithRetry-After(failed before the fix withserver error).docker exec summit-api bash -lc "cd /var/www && vendor/bin/phpunit tests/Unit/RateLimitMiddlewareTest.php"OAuth2SummitPromoCodesApiTest(34 tests, 3 skipped),OAuth2PromoCodesApiTest(40),PresentationReopenApiTest(33),PresentationReopenAuthzTest(14).tests/Unitran in full: 410 tests, 3 not passing, none in files this PR touches:CacheMiddlewareTest::test_lock_timeout_exception_handled_gracefully(mock cannot configurelock) and two inSponsorUserPermissionTrackingTest(testRemoveSponsorUserFromGroupStillCleansUpWhenSponsorWasDeleted,testRemoveSponsorUserFromGroupRemovesGlobalGroupWhenLastSponsor). They do not reference the rate limiter or the handler, but I did not re-run them onmainto confirm they fail there too.tests/oauth2as a whole (stopped after more than an hour). CI will cover those.Summary by CodeRabbit
429 Too Many Requestsresponse instead of being reported as a server error. JSON responses include retry timing information.