RSPEED-2413: replace BaseHTTPMiddleware with pure ASGI middleware#1133
RSPEED-2413: replace BaseHTTPMiddleware with pure ASGI middleware#1133major wants to merge 1 commit intolightspeed-core:mainfrom
Conversation
WalkthroughReplaces decorator-based request/response middlewares with two ASGI middleware classes—GlobalExceptionMiddleware and RestApiMetricsMiddleware—registered via app.add_middleware, converts logic to scope/receive/send handling, and updates unit tests to exercise ASGI message flows. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant GEM as GlobalExceptionMiddleware
participant RM as RestApiMetricsMiddleware
participant App as Inner ASGI App
participant Metrics as Metrics Collector
Client->>GEM: ASGI request (scope, receive, send)
GEM->>RM: forward scope/receive/send
RM->>App: forward scope/receive/send
alt App sends response events
App-->>RM: http.response.start / http.response.body
RM->>Metrics: record duration & status via send wrapper
RM-->>GEM: response events
GEM-->>Client: response delivered
else App raises uncaught exception
App--xRM: exception raised
RM->>Metrics: increment error counter (path/status)
RM-->>GEM: propagate exception
GEM->>Client: send 500 JSON error if response not started
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/app/main.py`:
- Around line 119-171: The RestApiMetricsMiddleware.__call__ currently awaits
self.app(scope, receive, send_wrapper) inside the duration context so if the
inner app raises the metrics.rest_api_calls_total.labels(path,
status_code).inc() is never executed; wrap the await in a try/finally (or ensure
a finally block) so that regardless of exceptions you still call
metrics.rest_api_calls_total.labels(path, status_code).inc() for measured paths
(excluding "/metrics"); keep using send_wrapper to capture the status_code and
reference path and app_routes_paths to determine whether to record the counter.
- Around line 225-229: Swap the registration order of the two middlewares so
RestApiMetricsMiddleware is registered last (making it the outermost middleware
at runtime): register GlobalExceptionMiddleware first and then call
app.add_middleware(RestApiMetricsMiddleware) after it, ensuring
RestApiMetricsMiddleware wraps GlobalExceptionMiddleware and can observe
responses (including 500s) emitted by GlobalExceptionMiddleware.
🧹 Nitpick comments (4)
src/app/main.py (1)
136-138: Pylint W0621 (appshadowing outer scope) — acceptable for ASGI middleware constructors.The
appparameter in__init__shadows the module-levelapp(the FastAPI instance). This is the standard ASGI middleware constructor signature and suppressing the warning is fine. The R0903 "too few public methods" warnings are also inherent to the ASGI middleware pattern. Consider adding a# pylint: disable=...inline or a project-level pylint config entry if you want a clean lint run.Also applies to: 182-184
tests/unit/app/test_main_middleware.py (3)
30-52:_ResponseCollectoris a handy test utility.The pipeline flags missing docstrings on
status_codeandbody_jsonproperties (C0116). Since these are test-internal helpers the impact is minimal, but adding one-liners would silence the linter.
110-112: Prefix unused ASGI parameters with_to silence W0613.The pipeline flags
scope,receive, andsendas unused in the inner test apps. Prefixing with_is the standard Python convention and would clean up the lint output.Example for one of the inner apps
- async def partial_response_app(scope: Scope, receive: Receive, send: Send) -> None: + async def partial_response_app(_scope: Scope, _receive: Receive, send: Send) -> None:Apply the same pattern to
inner_appin both non-HTTP tests (unusedscope,receive,send).Also applies to: 126-128, 145-147
140-151:RestApiMetricsMiddlewarehas only one test — consider adding coverage for the happy path and edge cases.The only test verifies non-HTTP passthrough. The following scenarios are untested at the unit level:
- Metrics counter increment — a request to a known app route results in
rest_api_calls_totalbeing incremented with the correct path and status./metricsendpoint exclusion — requests ending with/metricsdo not increment the counter.- Non-app-route passthrough — a path not in
app_routes_pathsbypasses metrics entirely.- Status code capture —
send_wrappercorrectly extracts the status fromhttp.response.start.These are the core behaviors of the middleware and are worth covering, especially since the class was just rewritten.
549dc1e to
74ff64f
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tests/unit/app/test_main_middleware.py`:
- Around line 155-166: The test
test_rest_api_metrics_increments_counter_on_exception currently only checks that
RuntimeError propagates; update it to also assert that the metrics counter was
incremented by patching or mocking
metrics.rest_api_calls_total.labels(...).inc() (or monkeypatching
RestApiMetricsMiddleware to use a mock counter) before invoking middleware, then
after the awaited call (inside the pytest.raises block) assert that the mock
inc() was called once with the expected label values; reference the test
function name and RestApiMetricsMiddleware and
metrics.rest_api_calls_total.labels(...).inc() to locate where to add the mock
and assertion.
🧹 Nitpick comments (1)
tests/unit/app/test_main_middleware.py (1)
40-45: Add docstrings to thestatus_codeandbody_jsonproperties.The linter flags
C0116: Missing function or method docstringon these properties. As per coding guidelines, "All functions require docstrings with brief descriptions and complete type annotations for parameters and return types".Proposed fix
`@property` def status_code(self) -> int: + """Return the HTTP status code from the collected response messages.""" for msg in self.messages: if msg["type"] == "http.response.start": return msg["status"] raise AssertionError("No http.response.start message") `@property` def body_json(self) -> dict: + """Return the response body decoded as JSON.""" body = b""
- Convert rest_api_metrics and global_exception_middleware from @app.middleware decorators to pure ASGI middleware classes - Eliminates call_next() which wraps handlers in anyio TaskGroup, causing "RuntimeError: No response returned" on long-running inference requests (15-30s) - Update middleware tests to use ASGI scope/receive/send protocol Signed-off-by: Major Hayden <major@redhat.com>
74ff64f to
2f62622
Compare
Description
Both
rest_api_metricsandglobal_exception_middlewareuse Starlette's@app.middleware()decorator, which wraps the endpoint inBaseHTTPMiddleware. This middleware usescall_next()internally, whichspawns the handler inside an anyio
TaskGroup. When inference requeststake 15-30s (normal for LLM + MCP tool calls), the
TaskGroupcontextloses the response reference, raising
RuntimeError: No response returnedinstead of returning the completed response.
This is a known limitation of
BaseHTTPMiddleware:Kludex/starlette#1914
This PR replaces both
@app.middleware()decorators with pure ASGImiddleware classes that call
self.app(scope, receive, send)directly.This eliminates
call_next(), theTaskGroupwrapping, and the responsebuffering — the response flows straight through the middleware chain
regardless of how long the handler takes.
Type of change
Tools used to create PR
Related Tickets & Documents
Checklist before requesting a review
Testing
POST /v1/inferreturns 500 withRuntimeError: No response returnedon every request (100% failure rate)POST /v1/inferreturns 200 with correct LLM response
confirming the issue was isolated to
BaseHTTPMiddlewareSummary by CodeRabbit