refactor: dedup auth + rate-limit boilerplate, add CLI git timeouts - #341
Closed
lfiaschi wants to merge 1 commit into
Closed
refactor: dedup auth + rate-limit boilerplate, add CLI git timeouts#341lfiaschi wants to merge 1 commit into
lfiaschi wants to merge 1 commit into
Conversation
High-leverage cleanups surfaced during a review pass: - rate_limit: bucket by the first X-Forwarded-For entry so clients behind Modal / CloudFlare do not all share a single limiter bucket (previously every user appeared as the proxy IP). Replace defaultdict with an explicit dict so blocked IPs do not leave empty list entries behind, and move purge cadence off a fragile `total % 100` modulo. - rate_limit: introduce rate_limit_dep() factory; collapses the seven copy-pasted _enforce_*_rate_limit helpers in registry_routes and search_routes into one-liners. - deps: factor a shared _decode_bearer helper so get_current_user and get_current_user_optional can not drift; former uses it to 401, the latter to return None on failure. - client git_repo: add a 5-minute timeout to every git subprocess call, catch TimeoutExpired / FileNotFoundError, and surface an actionable RuntimeError instead of hanging the CLI on a bad URL or dead mirror. - search_routes: extract _ref_from_row() so the main LLM-enrichment path and the Gemini-failure fallback share one ~15-field builder instead of two slightly-different copies. Tests cover each behaviour change: per-client bucketing via X-Forwarded-For, bucket cleanup, the dep factory, _decode_bearer error shapes, subprocess timeout surface, and _ref_from_row default-safety.
Contributor
Author
|
Closing as part of the 2026-08-11 open-PR consolidation. This automated review-sweep PR overlaps heavily with the retained merge queue (#449, #448, #447, #446, #405, then #438, #443, #375). Unique fixes not covered by the retained set are catalogued in #451 for a follow-up best-of PR. The branch is preserved, so nothing is lost. |
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
Focused cleanups surfaced during a principal-engineer-level review pass. Six high-leverage, low-risk fixes — each with tests — that touch the three most duplicated/most-dangerous spots in the backend + CLI without reaching into any god-modules or publish pipeline semantics.
Fixes
1. Rate limiter respects
X-Forwarded-For(bug)server/src/decision_hub/api/rate_limit.pyBehind Modal / CloudFlare,
request.client.hostis the proxy IP — so every user silently shared one 429 bucket and the limiter collapsed into a single global bucket. Now the limiter reads the first (left-most) entry ofX-Forwarded-Forwhen present and falls back to the direct peer otherwise. New test pins this behaviour so two distinct clients behind the same proxy get separate buckets.2. Rate limiter memory fix
Previously used
defaultdict(list), which left an empty list entry behind after pruning an expired IP — O(unique-IPs) leak. Replaced with an explicit dict that pops empty lists, and moved the purge cadence off the fragiletotal % 100trigger onto a dedicated counter that fires every 100 requests deterministically. New test asserts that idle IPs do not leave orphaned buckets.3. Dedup
_enforce_*_rate_limithelpersserver/src/decision_hub/api/registry_routes.py,search_routes.pySeven near-identical
_enforce_*_rate_limit(request)lazy-cache helpers collapsed into one-liners via a newrate_limit_dep(attr, limit_setting, window_setting)factory. Reduces registry_routes.py by ~80 lines.4. Dedup
get_current_user/get_current_user_optionalserver/src/decision_hub/api/deps.pyBoth dependencies now share one
_decode_bearerhelper (raises_AuthFailurewith a human-friendly message + outdated-token flag). The required variant maps failures toHTTPException(401); the optional variant returnsNone. New unit tests guarantee the two variants stay in lock-step.5. CLI: timeout every
gitsubprocessclient/src/dhub/core/git_repo.pyPreviously
subprocess.run(["git", "clone", ...])had notimeout=, so a bad URL or dead mirror would freeze the CLI session indefinitely. New_run_git()wrapper enforces a 5-minute timeout and surfacesTimeoutExpired/FileNotFoundErroras actionableRuntimeErrormessages. Tests simulate both failure modes.6. Dedup
AskSkillRefconstruction in/v1/askserver/src/decision_hub/api/search_routes.pyThe main LLM-enrichment path and the Gemini-failure fallback path each hand-rolled a 15-keyword-argument
AskSkillRef(...)literal; any new column drift-risked them apart. Extracted_ref_from_row()so both paths share one builder. Unit tests pin the default-safety contract.Why not more?
The review turned up two other bigger-leverage items that deserve their own PRs rather than getting buried here:
infra/database.py(3,465 lines / 103 functions) into a subpackage with a compat shim — high impact but large footprint; needs a staged landing.execute_publishtransaction boundary: commits mid-function, then opens a second connection to roll back on S3 upload failure. Documented behaviour (background eval thread needs the row visible), but there is a small race window. Fixing it correctly means threading a rollback callback out to the route — worth its own review cycle.Test plan
server/tests/test_api/test_rate_limit.py— 14 tests, all green (+7 new:_client_key, X-Forwarded-For bucketing, memory cleanup,rate_limit_depfactory)server/tests/test_api/test_deps.py— 10 tests, all green (+7 new for_decode_bearer+TestCurrentUserDepsparity)server/tests/test_api/test_search_routes.py— 16 tests, all green (+3 new for_ref_from_row)client/tests/test_core/test_git_repo.py— 12 tests, all green (+3 new for clone timeout / missing-git handling)server/tests/953 passed,client/tests/294 passed,shared/tests/98 passedruff check+ruff format --checkclean across the whole repohttps://claude.ai/code/session_01HXYZgPWgCkttGFAc8hru3p