Sanitize server file paths from validation error responses (#46415)

Signed-off-by: muhammadfawaz1 <135441198+muhammadfawaz1@users.noreply.github.com> Co-authored-by: Mahad Durrani <114791389+mahadrehmann@users.noreply.github.com>

Muhammad Fawaz committed Jul 9, 2026 at 02:46 UTC e87521626febe2763f997691d1599de4175f4324
4 files changed +114 -8
tests/entrypoints/serve/utils/test_api_utils.py
+46
@@ -118,3 +118,49 @@ class TestGetMaxTokens:
118 input_length=150,
119 default_sampling_params={"max_tokens": 2048},
120 )
121 +
122 +
123 +class TestSanitizeMessageFilePaths:
124 + """sanitize_message should also strip file paths and traceback
125 + frames, not just memory addresses - see #31683."""
126 +
127 + def test_strips_traceback_style_frame(self):
128 + msg = (
129 + "1 validation error:\n"
130 + " {'type': 'list_type', 'loc': ('body', 'messages')}\n"
131 + '\n File "/usr/local/lib/python3.12/dist-packages/vllm/'
132 + 'entrypoints/serve/utils/api_utils.py", line 40, '
133 + "in create_chat_completion\n"
134 + " POST /v1/chat/completions"
135 + )
136 + result = sanitize_message(msg)
137 + assert "/usr/local/" not in result
138 + assert "api_utils.py" not in result
139 + assert "list_type" in result
140 +
141 + def test_strips_arbitrary_absolute_path(self):
142 + result = sanitize_message("Error in /home/user/project/vllm/server.py")
143 + assert "/home/user" not in result
144 +
145 + def test_strips_single_parent_container_path(self):
146 + """Regression: /app/server.py and /workspace/server.py (common in
147 + container deployments) were missed by the original {2,} quantifier."""
148 + assert "/app/" not in sanitize_message("Error in /app/server.py")
149 + assert "/workspace/" not in sanitize_message("Error in /workspace/server.py")
150 +
151 + def test_preserves_api_endpoint_paths(self):
152 + msg = "POST /v1/chat/completions failed"
153 + assert "/v1/chat/completions" in sanitize_message(msg)
154 +
155 + def test_preserves_short_field_references(self):
156 + msg = "Invalid value for field 'body.messages'"
157 + assert sanitize_message(msg) == msg
158 +
159 + def test_strips_both_address_and_path(self):
160 + msg = (
161 + "<Request at 0x7f123> failed at "
162 + "/usr/local/lib/python3.12/dist-packages/vllm/server.py"
163 + )
164 + result = sanitize_message(msg)
165 + assert "0x" not in result
166 + assert "/usr/local/" not in result
tests/entrypoints/serve/utils/test_server_utils.py
+51
@@ -100,3 +100,54 @@ class TestCleanLocForParam:
100 dot-join rather than returning an empty string."""
101 loc = ("function-wrap[__log_extra_fields__()]",)
102 assert clean_loc_for_param(loc) == "function-wrap[__log_extra_fields__()]"
103 +
104 +
105 +class TestValidationErrorDoesNotLeakServerPaths:
106 + """FastAPI sets endpoint_file/line/function/path on the exception
107 + during routing, not in the constructor - so we set them manually
108 + here to reproduce the real leak. See #31683."""
109 +
110 + @pytest.mark.asyncio
111 + async def test_handler_strips_endpoint_file_context(self):
112 + errors = [
113 + {
114 + "type": "list_type",
115 + "loc": ("body", "messages"),
116 + "msg": "Input should be a valid list",
117 + "input": "not-a-list",
118 + }
119 + ]
120 + exc = RequestValidationError(errors)
121 + exc.endpoint_file = (
122 + "/usr/local/lib/python3.12/dist-packages/vllm/"
123 + "entrypoints/serve/utils/api_utils.py"
124 + )
125 + exc.endpoint_line = 40
126 + exc.endpoint_function = "create_chat_completion"
127 + exc.endpoint_path = "POST /v1/chat/completions"
128 +
129 + response = await validation_exception_handler(_fake_request(), exc)
130 + body = json.loads(response.body)
131 + message = body["error"]["message"]
132 +
133 + assert "/usr/local/" not in message
134 + assert "api_utils.py" not in message
135 + assert "create_chat_completion" not in message
136 + assert "list_type" in message
137 + assert "Input should be a valid list" in message
138 +
139 + @pytest.mark.asyncio
140 + async def test_handler_strips_endpoint_path_only_variant(self):
141 + """Covers the branch where only endpoint_path is set."""
142 + errors = [
143 + {"type": "missing", "loc": ("body", "messages"), "msg": "Field required"}
144 + ]
145 + exc = RequestValidationError(errors)
146 + exc.endpoint_path = "POST /v1/chat/completions"
147 +
148 + response = await validation_exception_handler(_fake_request(), exc)
149 + body = json.loads(response.body)
150 + message = body["error"]["message"]
151 +
152 + assert "missing" in message
153 + assert "Field required" in message
vllm/entrypoints/serve/utils/api_utils.py
+8 -2
@@ -310,8 +310,14 @@ def process_lora_modules(
310
311
312 def sanitize_message(message: str) -> str:
313 - # Avoid leaking memory address from object reprs
314 - return re.sub(r" at 0x[0-9a-f]+>", ">", message)
313 + """Strip memory addresses, tracebacks, and file paths from error messages."""
314 + message = re.sub(r" at 0x[0-9a-f]+>", ">", message)
315 + message = re.sub(r'\n?\s*File "[^"]+", line \d+, in \S+(\n\s+.*)?', "", message)
316 + message = re.sub(
317 + r"/(?:home|usr|opt|var|tmp|root|lib|mnt|srv)(?:/[\w.\-]+)+", "<path>", message
318 + )
319 + message = re.sub(r"(?:/[\w\-]+)+/[\w\-]+\.\w+", "<path>", message)
320 + return message.strip()
321
322
323 def log_version_and_model(lgr: Logger, version: str, model_name: str) -> None:
vllm/entrypoints/serve/utils/server_utils.py
+9 -6
@@ -509,13 +509,16 @@ async def validation_exception_handler(req: Request, exc: RequestValidationError
509 if loc:
510 param = clean_loc_for_param(loc)
511
512 - exc_str = str(exc)
513 - errors_str = str(errors)
514 -
515 - if errors and errors_str and errors_str != exc_str:
516 - message = f"{exc_str} {errors_str}"
512 + # Build the message from exc.errors() instead of str(exc) - str(exc)
513 + # leaks the server's file path via FastAPI's endpoint context.
514 + if errors:
515 + count = len(errors)
516 + label = "error" if count == 1 else "errors"
517 + message = f"{count} validation {label}:\n"
518 + message += "".join(f" {err}\n" for err in errors)
519 + message = message.rstrip()
520 else:
518 - message = exc_str
521 + message = "Validation error"
522
523 err = ErrorResponse(
524 error=ErrorInfo(