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(