[Bugfix][Frontend] Answer rejected requests without a server traceback (#1399)
A request rejected by validation, such as a prompt longer than max_model_len, got its 400 response and then a full "Exception in ASGI application" traceback in the server log: the only handler that matched it was registered for Exception, and Starlette runs that one in ServerErrorMiddleware, which re-raises after responding. The 1.5.2 release matrix logged 44 of them. VLLMValidationError and VLLMNotFoundError now have their own registrations, so they are answered and not re-raised. Other exceptions still reach the server log. Handler registration moves to register_exception_handlers() so tests can build the same app. Co-authored-by: mac <mac@MacBook-xinyi.local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+60
-16
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Rejected requests are answered without re-raising into the ASGI server."""
|
||||
|
||||
from argparse import Namespace
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from vllm.entrypoints.openai.server_utils import register_exception_handlers
|
||||
from vllm.exceptions import VLLMNotFoundError, VLLMValidationError
|
||||
|
||||
|
||||
def _client() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.state.args = Namespace(log_error_stack=False)
|
||||
register_exception_handlers(app)
|
||||
|
||||
@app.get("/raise/{kind}")
|
||||
async def fail(kind: str):
|
||||
if kind == "validation":
|
||||
raise VLLMValidationError("prompt is longer than the maximum length")
|
||||
if kind == "not_found":
|
||||
raise VLLMNotFoundError("unknown model")
|
||||
raise RuntimeError("server bug")
|
||||
|
||||
# A re-raised exception surfaces here exactly as uvicorn would log it.
|
||||
return TestClient(app, raise_server_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("kind", "status"), [("validation", 400), ("not_found", 404)])
|
||||
def test_client_errors_are_answered_without_a_traceback(kind, status):
|
||||
response = _client().get(f"/raise/{kind}")
|
||||
assert response.status_code == status
|
||||
assert response.json()["error"]["code"] == status
|
||||
|
||||
|
||||
def test_server_errors_still_reach_the_server_log():
|
||||
with pytest.raises(RuntimeError, match="server bug"):
|
||||
_client().get("/raise/bug")
|
||||
@@ -16,8 +16,7 @@ from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
import uvloop
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.datastructures import State
|
||||
|
||||
@@ -29,18 +28,13 @@ from vllm.entrypoints.chat_utils import load_chat_template
|
||||
from vllm.entrypoints.launcher import serve_http
|
||||
from vllm.entrypoints.logger import RequestLogger
|
||||
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
|
||||
from vllm.entrypoints.openai.engine.protocol import GenerationError
|
||||
from vllm.entrypoints.openai.models.protocol import BaseModelPath
|
||||
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
|
||||
from vllm.entrypoints.openai.server_utils import (
|
||||
engine_error_handler,
|
||||
exception_handler,
|
||||
generation_error_handler,
|
||||
get_uvicorn_log_config,
|
||||
http_exception_handler,
|
||||
lifespan,
|
||||
log_response,
|
||||
validation_exception_handler,
|
||||
register_exception_handlers,
|
||||
)
|
||||
from vllm.entrypoints.sagemaker.api_router import sagemaker_standards_bootstrap
|
||||
from vllm.entrypoints.serve.elastic_ep.middleware import (
|
||||
@@ -63,7 +57,6 @@ from vllm.usage.usage_lib import UsageContext
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
from vllm.utils.network_utils import is_valid_ipv6_address
|
||||
from vllm.utils.system_utils import decorate_logs, set_ulimit
|
||||
from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
|
||||
from vllm.version import __version__ as VLLM_VERSION
|
||||
|
||||
prometheus_multiproc_dir: tempfile.TemporaryDirectory
|
||||
@@ -254,12 +247,7 @@ def build_app(
|
||||
allow_headers=args.allowed_headers,
|
||||
)
|
||||
|
||||
app.exception_handler(HTTPException)(http_exception_handler)
|
||||
app.exception_handler(RequestValidationError)(validation_exception_handler)
|
||||
app.exception_handler(EngineGenerateError)(engine_error_handler)
|
||||
app.exception_handler(EngineDeadError)(engine_error_handler)
|
||||
app.exception_handler(GenerationError)(generation_error_handler)
|
||||
app.exception_handler(Exception)(exception_handler)
|
||||
register_exception_handlers(app)
|
||||
|
||||
# Ensure --api-key option from CLI takes precedence over VLLM_API_KEY
|
||||
if tokens := [key for key in (args.api_key or [envs.VLLM_API_KEY]) if key]:
|
||||
|
||||
@@ -27,7 +27,7 @@ from vllm.entrypoints.openai.engine.protocol import (
|
||||
GenerationError,
|
||||
)
|
||||
from vllm.entrypoints.utils import create_error_response, sanitize_message
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.exceptions import VLLMNotFoundError, VLLMValidationError
|
||||
from vllm.logger import init_logger
|
||||
from vllm.utils.gc_utils import freeze_gc_heap
|
||||
from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
|
||||
@@ -388,6 +388,21 @@ async def exception_handler(req: Request, exc: Exception):
|
||||
return JSONResponse(err.model_dump(), status_code=err.error.code)
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI) -> None:
|
||||
app.exception_handler(HTTPException)(http_exception_handler)
|
||||
app.exception_handler(RequestValidationError)(validation_exception_handler)
|
||||
app.exception_handler(EngineGenerateError)(engine_error_handler)
|
||||
app.exception_handler(EngineDeadError)(engine_error_handler)
|
||||
app.exception_handler(GenerationError)(generation_error_handler)
|
||||
# Starlette runs a handler registered for Exception in its
|
||||
# ServerErrorMiddleware, which re-raises after responding: uvicorn then logs
|
||||
# a full traceback for every rejected request. Client errors get their own
|
||||
# registration so they are answered without being re-raised.
|
||||
app.exception_handler(VLLMValidationError)(exception_handler)
|
||||
app.exception_handler(VLLMNotFoundError)(exception_handler)
|
||||
app.exception_handler(Exception)(exception_handler)
|
||||
|
||||
|
||||
async def http_exception_handler(req: Request, exc: HTTPException):
|
||||
if req.app.state.args.log_error_stack:
|
||||
logger.exception(
|
||||
|
||||
Reference in new issue
Block a user