Repository navigation
fix(rate-limiting): enforce the per-user limit on chat sends, and record why the other two stay unwired (#16857) - #17874
Conversation
…on chat sends (#16857) user_rate_limiter and conversation_rate_limiter were built (#4460) and never acquired, so no per-user or per-conversation request cap ran on chat. Per the owner decision on #16857 both are now enforced on the message-send endpoints only, via a router-level dependency that acts on POSTs to the named send handlers; a route-wide cap would throttle the UI's own polling. api/chat.py is at its file-size ceiling, so the dependency is attached at mount time: route_dependencies maps a registry router name to dependencies, and app_factory passes them to include_router. The registry tuple shape is unchanged. llm_rate_limiter is deliberately not wired: provider pacing is LLMCrossWorkerRateLimiter, and its module now says so. That limiter does not cover stream_completion -- recorded on #16538, the existing home. Tests run the shared limiter's real Lua window on fakeredis and assert the request past the limit gets 429, per the issue's criterion.
…er only on sends (#16857) From the pre-push review: - the conversation limit was keyed by a conversation id taken from the request and taken before the endpoint's ownership check, so any user could exhaust another's conversation and lock the owner out with 429. Owner decision: drop it; the per-user limit bounds each caller. conversation_rate_limiter's module now says why it is unwired. - the dependency declared Depends(get_current_user) router-wide, adding an auth requirement to chat_sessions routes that resolve the user another way. It now returns before resolving the caller unless the request is a send, so no other route changes. - route_dependencies imports the dependency at load time: lazily, an import error would have silently unmounted the whole chat router. - tests: a flood by one user does not lock out the owner; a non-send route never resolves the caller; a caller without user_id is not counted; the loop bound reads the live tier config. 6 pass; with the mapping emptied, the two refusal tests fail.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds per-user rate limiting for selected POST chat endpoints. The chat router receives the limiter through router-level dependencies. Tests cover rate-limit responses, excluded routes, and caller identity handling. ChangesChat-send rate limiting
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The chat-send limit is wired to the production router, with no additional merge-blocking issue established. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change adds useful throttling without demonstrated expansion of access or cross-user budget consumption. It is not a universal abuse limit: authenticated identities without a user ID and requests during Redis failures remain unthrottled, and strict concurrent accounting is not fully established. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
Review profile: ASSERTIVE
Plan: Advanced
Run ID: 31a774f2-ea1d-4273-8d0b-2ea20cf83948
📒 Files selected for processing (6)
autobot-backend/api/chat_send_rate_limit.pyautobot-backend/api/chat_send_rate_limit_test.pyautobot-backend/app_factory.pyautobot-backend/initialization/router_registry/route_dependencies.pyautobot-backend/llm_shared/optimization/rate_limiter.pyautobot-backend/utils/conversation_rate_limiter.py
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
…e real chat router (#16857)
…#16857) The runtime-name test read chat_router.routes. api.chat includes child routers, and under fastapi>=0.139 that list holds opaque wrappers for them, so the read could find nothing and pass vacuously -- repo_tests guard router_routes_traversal_test (#15093) failed it in CI shard 12. Use the shared effective_routes() traversal, and add a floor so an empty walk fails.
Thinking Path
#16857: three instances of the shared
autobot_shared.rate_limiter.RateLimiterwere built (#4460) and never acquired, so the controls they imply never ran. Measured first: there is one shared implementation, so nothing needed consolidating; the gap is wiring only.ssot_configdefines none of therate_limit_*keys, so the tier defaults apply (authenticated: 60/min, 2000/h).Whether each surface should be limited is a product decision (the issue's open question). Owner decisions, recorded on #16857:
user_rate_limiterconversation_rate_limiterllm_rate_limiterLLMCrossWorkerRateLimiter; two budgets over one limit would leave no authority. That limiter does not coverstream_completionin any of the 4 providers, and streaming is chat's default path. Recorded on #16538, the existing home for that block (one defect, one fix).Each unwired instance now states why in its own module, which is the issue's stated resolution for "not wanted".
Hook point (owner decision):
api/chat.pyis pinned at its 2778-line ceiling and every send route lives there. The limit is a router-level dependency attached at mount time:route_dependencies.dependencies_for(name)is consulted byapp_factory._register_routers. The registry 4-tuple shape is unchanged, andcore_routers.py(590/600) is untouched. The dependency returns before resolving the caller unless the request is a POST to a named send endpoint, so no other chat route gains an auth requirement or a cap. It is imported eagerly, so a broken import fails startup instead of silently unmounting the chat router.What Changed
autobot-backend/api/chat_send_rate_limit.py(new):SEND_ENDPOINTSandenforce_chat_send_rate_limit. Past the limit it returns 429 withRetry-After. A caller withoutuser_id(the internal-service identity) is not counted. A Redis outage fails open, per the shared limiter's policy.autobot-backend/initialization/router_registry/route_dependencies.py(new): a name-keyed mapping of router-level dependencies.autobot-backend/app_factory.py: passesdependencies=dependencies_for(name)toinclude_routerfor core routers.autobot-backend/utils/conversation_rate_limiter.py,autobot-backend/llm_shared/optimization/rate_limiter.py: comments recording why each instance is unwired.autobot-backend/api/chat_send_rate_limit_test.py(new): 7 tests. The shared limiter's real Lua window runs on fakeredis, and the assertions are HTTP status codes, not calls toacquire:Retry-After;user_idis not counted;SEND_ENDPOINTSis pinned againstapi/chat.py's POST routes, in two layers:router.postfunction in the file (AST).9532cbc39c): the realapi.chatrouter is imported, and every name must be the__name__of a registered POST route after its decorators._is_sendmatches the runtime name, so this is the check that actually guards it. A decorator that stopped preserving__name__would silently unlimit every send route while the source check stayed green.Verification
/optPython 3.14 interpreter, read-only, with the worktree onPYTHONPATHand test-only deps in a scratch dir (nothing written under/opt):6 passed. Mutation: with the registry mapping emptied (limiter unwired),2 failed, 4 passed; the two 429 tests catch an unwired limiter./opt's packages sit below some declared floors, so CI is the evidence.Single-issue rationale: the sibling search (open issues on these files; rate limit, throttle, backoff) found none sharing this file set. The related stream_completion gap (#16538) is a provider-behaviour change with a different risk profile, so it is recorded there rather than batched here.
Closes #16857
Model Used
Claude Opus 5.5
🤖 Generated with Claude Code
Summary by CodeRabbit