Skip to content

fix(controller): forward upstream error status and body - #616

Open
Lumea-art wants to merge 1 commit into
ovg-project:mainfrom
Lumea-art:fix/controller-error-status
Open

Lumea-art wants to merge 1 commit into
ovg-project:mainfrom
Lumea-art:fix/controller-error-status

Conversation

@Lumea-art

Copy link
Copy Markdown
Contributor

What

Follow-up to the merged #615 (same file family).

Every routing failure collapsed into the same response:

HTTP/1.1 500
{"error": "Failed to route request for model ..."}

Measured against a real vLLM 0.29 backend:

request backend answers controller answered
temperature: -5 400 + {"error":{"message":"temperature must be non-negative...","type":"BadRequestError","param":"temperature","code":400}} 500 + Failed to route request for model ...
unknown model — 500 (should be 404)
malformed JSON — 500 (should be 400)
engine down — 500 with the same generic text

Clients cannot tell a bad parameter from a routing failure, cannot show the engine's message, and cannot decide whether to retry.

Changes

controller/router.py

  • route_request() raises a typed RouteError instead of returning None:
    • UpstreamError(status, body, content_type) — the engine answered non-2xx; its status, OpenAI-shaped body and content type are preserved. Applies to both branches; the streaming branch used to drop the body entirely and report a rejected stream as a generic 500;
    • ModelNotFoundError -> 404, BackendUnavailableError -> 503 (wake-up failure, unreachable engine, closed session), GatewayTimeoutError -> 504.
  • _read_error_body() keeps a non-JSON engine body (plain-text 5xx) by wrapping it in the OpenAI error schema.
  • traffic monitoring still records every attempt, including the new failures.

controller/frontend.py

  • RouteError is translated to its status and payload; json.JSONDecodeError becomes 400 BadRequestError; unexpected exceptions stay 500 but now use the OpenAI error schema too.

Tests

tests/test_controller_errors.py (registered in tests/manifests/cpu.txt):

  • 404 / 503 / 504 mapping for both completion endpoints;
  • upstream 400 body forwarded verbatim, 429 not rewritten to 500;
  • malformed JSON -> 400;
  • unexpected failure -> 500 with the OpenAI schema;
  • router against a fake aiohttp engine: 200 passthrough, 400/503 body preservation, unknown model, and an unreachable port.

Verification

pytest tests/test_controller_errors.py tests/test_frontend_streaming.py \
       tests/test_sleep_manager.py tests/test_traffic_monitor.py
# 46 passed

ruff check / isort --check-only / mypy tests/test_controller_errors.py   # clean
python tools/check_test_classification.py                                # valid, 111 tests

Live check on a two-model vLLM 0.29 + kvcached stack (one RTX 5090), same requests before and after:

request before after
invalid temperature 500 generic 400 + the engine's exact message
unknown model 500 generic 404 NotFoundError
malformed JSON 500 400 BadRequestError
dead backend 500 generic 503 ServiceUnavailableError
stream + invalid temperature 500 generic 400 + the engine's message
normal / streaming 200 200 (9 SSE events)

Every routing failure used to collapse into the same 500 with
{"error": "Failed to route request for model ..."}: an invalid sampling
parameter, an unknown model and an unreachable engine were indistinguishable
to clients, and the engine's OpenAI-shaped error body was dropped.

route_request() now raises a typed RouteError instead of returning None:

- UpstreamError carries the engine's own status, body and content type, so a
  400/404/429/503 passes through unchanged; that includes the streaming
  branch, which used to report a rejected stream as a generic 500;
- ModelNotFoundError, BackendUnavailableError and GatewayTimeoutError map to
  404, 503 and 504 for router-side failures;
- the frontend answers malformed JSON with 400 instead of 500 and keeps the
  OpenAI error schema for its own 500s.

Traffic monitoring still records every attempt, including the new failure
paths.

Add tests/test_controller_errors.py: the status mapping, the verbatim upstream
body, malformed JSON, a fake engine returning 400/503, an unknown model and an
unreachable endpoint.
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.

1 participant