Skip to content

refactor: dedup auth + rate-limit boilerplate, add CLI git timeouts - #341

Closed
lfiaschi wants to merge 1 commit into
mainfrom
claude/adoring-ride-zwmRP
Closed

refactor: dedup auth + rate-limit boilerplate, add CLI git timeouts#341
lfiaschi wants to merge 1 commit into
mainfrom
claude/adoring-ride-zwmRP

Conversation

@lfiaschi

Copy link
Copy Markdown
Contributor

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.py

Behind Modal / CloudFlare, request.client.host is 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 of X-Forwarded-For when 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 fragile total % 100 trigger 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_limit helpers

server/src/decision_hub/api/registry_routes.py, search_routes.py

Seven near-identical _enforce_*_rate_limit(request) lazy-cache helpers collapsed into one-liners via a new rate_limit_dep(attr, limit_setting, window_setting) factory. Reduces registry_routes.py by ~80 lines.

4. Dedup get_current_user / get_current_user_optional

server/src/decision_hub/api/deps.py

Both dependencies now share one _decode_bearer helper (raises _AuthFailure with a human-friendly message + outdated-token flag). The required variant maps failures to HTTPException(401); the optional variant returns None. New unit tests guarantee the two variants stay in lock-step.

5. CLI: timeout every git subprocess

client/src/dhub/core/git_repo.py

Previously subprocess.run(["git", "clone", ...]) had no timeout=, so a bad URL or dead mirror would freeze the CLI session indefinitely. New _run_git() wrapper enforces a 5-minute timeout and surfaces TimeoutExpired / FileNotFoundError as actionable RuntimeError messages. Tests simulate both failure modes.

6. Dedup AskSkillRef construction in /v1/ask

server/src/decision_hub/api/search_routes.py

The 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:

  • Split 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_publish transaction 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_dep factory)
  • server/tests/test_api/test_deps.py — 10 tests, all green (+7 new for _decode_bearer + TestCurrentUserDeps parity)
  • 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)
  • Full suites: server/tests/ 953 passed, client/tests/ 294 passed, shared/tests/ 98 passed
  • ruff check + ruff format --check clean across the whole repo

https://claude.ai/code/session_01HXYZgPWgCkttGFAc8hru3p

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.
@lfiaschi

Copy link
Copy Markdown
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.

@lfiaschi lfiaschi closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants