Repository navigation
Add Retry-After to rate-limited responses - #771
iacobdaniel wants to merge 1 commit into
Conversation
| self.rate_limited_items = LRUCache(max_items, time_to_live_in_ms) | ||
|
|
||
| def is_allowed(self, key, window_size_in_ms, max_requests): | ||
| def check(self, key, window_size_in_ms, max_requests): |
There was a problem hiding this comment.
RateLimiter.check starts with check, which obscures that it both records a request and returns a rate-limit decision.
Details
✨ AI Reasoning
The new method is named check, which starts with the discouraged prefix. The name does not communicate whether the method only evaluates a decision or also records request timestamps; its implementation does both.
🔧 How do I fix it?
Replace 'check' with more descriptive verbs that indicate the function's action or purpose. Use 'validate' for validation, 'get' for retrieval, or 'is' for boolean checks. Ensure the name clearly communicates the function's intent and return type.
Reply on this comment to give feedback or ignore the issue.
More info
| ) | ||
| if not allowed: | ||
| return {"block": True, "trigger": "group"} | ||
| if not result["allowed"]: |
There was a problem hiding this comment.
The group, user, and IP branches repeat the same blocked-result construction, including retry_after_seconds. Consolidate the shared handling so changes to this response stay consistent across all three paths.
Details
✨ AI Reasoning
The group, user, and IP branches each inspect the same allowed result and construct the same blocked response shape, differing only in the key and trigger. This repeats the newly added retry-delay handling and response construction, so consolidating that shared behavior would reduce the chance that one path is updated differently.
🔧 How do I fix it?
Delete extra code. Extract repeated code sequences into reusable functions or methods. Use loops or data structures to eliminate repetitive patterns.
Reply on this comment to give feedback or ignore the issue.
More info
| # Blocked requests are stored too, so this timestamp, not the oldest one, must | ||
| # leave the window first. max() keeps the index valid when maxRequests < 1. | ||
| blocking_timestamp = request_timestamps[-max(max_requests, 1)] | ||
|
|
||
| # A timestamp exactly one window old still counts, so the wait must be longer. | ||
| retry_after_ms = blocking_timestamp + window_size_in_ms - current_time | ||
| return { | ||
| "allowed": False, | ||
| "retry_after_seconds": max(1, retry_after_ms // 1000 + 1), | ||
| } |
There was a problem hiding this comment.
🔵 Low - Zero-request limits advertise a retry that can never succeed
When the service supplies an endpoint with maxRequests set to zero, every request is denied by the new limiter. check nevertheless returns a finite Retry-After, and after that delay the window is empty but the new request is still denied, so clients following the advertised delay can never get through.
Show fix
| # Blocked requests are stored too, so this timestamp, not the oldest one, must | |
| # leave the window first. max() keeps the index valid when maxRequests < 1. | |
| blocking_timestamp = request_timestamps[-max(max_requests, 1)] | |
| # A timestamp exactly one window old still counts, so the wait must be longer. | |
| retry_after_ms = blocking_timestamp + window_size_in_ms - current_time | |
| return { | |
| "allowed": False, | |
| "retry_after_seconds": max(1, retry_after_ms // 1000 + 1), | |
| } | |
| if max_requests < 1: | |
| return {"allowed": False, "retry_after_seconds": 0} | |
| # Blocked requests are stored too, so this timestamp, not the oldest one, must | |
| # leave the window first. | |
| blocking_timestamp = request_timestamps[-max_requests] | |
| # A timestamp exactly one window old still counts, so the wait must be longer. | |
| retry_after_ms = blocking_timestamp + window_size_in_ms - current_time | |
| return { | |
| "allowed": False, | |
| "retry_after_seconds": max(1, retry_after_ms // 1000 + 1), | |
| } |
More info - Reply on this comment to give feedback or ignore the issue.
Rate-limited requests now get a
Retry-Afterheader on Zen's 429 from the Flask, Django and ASGI middlewares, andshould_block_request()returnsretry_after_secondsso custom middleware can set it too. Blocked requests also count in the rate-limit window, so the delay is measured from themax_requests-th newest request in that window: a client that waitsRetry-Afterseconds is let through, and one second earlier it is still blocked.RateLimiter.is_allowed()is renamed tocheck(), because it now returns the delay with the decision instead of a bool, and the QA test forRetry-Afteris no longer skipped.