From 4bd121493da37c212462a1b1b8f1d0fb44caae18 Mon Sep 17 00:00:00 2001 From: AxelRay Date: Sun, 26 Jul 2026 04:17:40 +0700 Subject: [PATCH] fix(proxy): allow request_scope import without fastapi (#2562) ## Description Base installs without the `proxy` extra crash during CLI command registration because `headroom.proxy.request_scope` imported FastAPI at module import time. That import is only needed for typing on `normalize_request_path`. This change keeps the FastAPI `Request` import under `TYPE_CHECKING` so the CLI path used by `headroom --help` no longer requires FastAPI. Closes #2561 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Refactoring (no functional changes) - [ ] Performance improvement - [ ] Test update - [ ] Build/CI change - [ ] Other (please describe): ## Changes Made - Make the FastAPI `Request` import type-checking only in `headroom/proxy/request_scope.py` - Add a subprocess regression test that imports `request_scope` and `project_context` with FastAPI blocked and verifies `normalize_scope_path` ## Testing ### Test commands run ```bash PYTHONPATH=. python3 -m pytest tests/test_proxy_request_scope.py tests/test_request_scope_no_fastapi.py -q ruff format --check headroom/proxy/request_scope.py tests/test_request_scope_no_fastapi.py ruff check headroom/proxy/request_scope.py tests/test_request_scope_no_fastapi.py ``` ### Test Output ```text ========================= 5 passed, 1 warning in 0.93s ========================= 2 files already formatted All checks passed! ``` ## Real Behavior Proof ### Environment - Linux x86_64, Python 3.11.15 - Shallow sparse checkout of headroom main at commit parent of this PR - System/Hermes venv Python with pytest and ruff available ### Exact command ```bash PYTHONPATH=. python3 - <<'PY' import builtins, sys real = builtins.__import__ def imp(name, *a, **k): if name == "fastapi" or name.startswith("fastapi."): raise ModuleNotFoundError("No module named 'fastapi'") return real(name, *a, **k) builtins.__import__ = imp import headroom.proxy.request_scope as rs import headroom.proxy.project_context as pc rs.normalize_scope_path({"path": "/a"}, "/b") print("ok", "fastapi" not in sys.modules, hasattr(pc, "with_project_prefix")) PY ``` ### Observed result ```text ok True True ``` Importing the request-scope helpers no longer requires FastAPI, and scope path normalization still works. ### Not tested - Full base `pip install headroom-ai` (no extras) end-to-end on a clean venv without the monorepo source tree - Full monorepo `make ci-precheck` / cargo workspace - Live proxy traffic or FastAPI request path behavior beyond the existing unit test for `normalize_request_path` ## Review Readiness - [x] I have tested these changes locally - [x] I have added/updated tests where applicable - [x] I have updated documentation if needed (N/A) - [x] My code follows the project's style guidelines - [x] I have run linting/formatting checks - [x] I have considered security implications - [x] This PR is ready for review --- headroom/proxy/request_scope.py | 5 +-- tests/test_request_scope_no_fastapi.py | 42 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/test_request_scope_no_fastapi.py diff --git a/headroom/proxy/request_scope.py b/headroom/proxy/request_scope.py index 4a7e14dc9..dc8e9e1db 100644 --- a/headroom/proxy/request_scope.py +++ b/headroom/proxy/request_scope.py @@ -3,10 +3,11 @@ from __future__ import annotations from collections.abc import MutableMapping -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import quote -from fastapi import Request +if TYPE_CHECKING: + from fastapi import Request def normalize_scope_path(scope: MutableMapping[str, Any], path: str) -> None: diff --git a/tests/test_request_scope_no_fastapi.py b/tests/test_request_scope_no_fastapi.py new file mode 100644 index 000000000..12b337d7f --- /dev/null +++ b/tests/test_request_scope_no_fastapi.py @@ -0,0 +1,42 @@ +"""Regression tests for importing request-scope helpers without FastAPI.""" + +import subprocess +import sys +import textwrap + + +def test_request_scope_and_project_context_import_without_fastapi() -> None: + script = textwrap.dedent( + """ + import builtins + import sys + + real_import = builtins.__import__ + + def import_without_fastapi(name, *args, **kwargs): + if name == "fastapi" or name.startswith("fastapi."): + raise ModuleNotFoundError("No module named 'fastapi'") + return real_import(name, *args, **kwargs) + + builtins.__import__ = import_without_fastapi + + import headroom.proxy.request_scope as request_scope + import headroom.proxy.project_context + + assert not any( + name == "fastapi" or name.startswith("fastapi.") for name in sys.modules + ) + scope = {"path": "/a", "raw_path": b"/a"} + request_scope.normalize_scope_path(scope, "/b c") + assert scope == {"path": "/b c", "raw_path": b"/b%20c"} + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + timeout=30, + ) + + assert result.returncode == 0, result.stderr