[Frontend] Use VLLMValidationError for batch request URL validation (#50191)
Signed-off-by: Chao Tan <chaos.tc@gmail.com>
Tan Chao committed
Aug 21, 2026 at 09:07 UTC
1baf372bfc14d739860b4a7122877e22ef0dcbf1
2 files changed
+36
-13
tests/entrypoints/openai/test_run_batch.py
+20
-7
@@ -13,6 +13,7 @@ from vllm.entrypoints.openai.run_batch import (
13
BatchRequestOutput,
14
download_bytes_from_url,
15
)
16
+from vllm.exceptions import VLLMValidationError
17
18
CHAT_MODEL_NAME = "hmellor/tiny-random-LlamaForCausalLM"
19
EMBEDDING_MODEL_NAME = "intfloat/multilingual-e5-small"
@@ -788,15 +789,27 @@ async def test_download_bytes_data_url_bypasses_domain_check():
789
async def test_download_bytes_rejects_disallowed_domain():
790
"""HTTP URLs whose hostname is not in the allowlist must be rejected."""
791
url = "https://evil.internal/secret"
791
- with pytest.raises(ValueError, match="allowed domains"):
792
+ with pytest.raises(VLLMValidationError, match="allowed domains") as exc_info:
793
await download_bytes_from_url(url, allowed_media_domains=["example.com"])
794
+ # URL validation failures carry structured metadata for the frontend.
795
+ assert exc_info.value.parameter == "url"
796
+ assert exc_info.value.value == "evil.internal"
797
+
798
+
799
+@pytest.mark.asyncio
800
+async def test_download_bytes_rejects_unsupported_scheme():
801
+ """Unsupported URL schemes are rejected with structured metadata."""
802
+ with pytest.raises(VLLMValidationError, match="Unsupported URL scheme") as exc_info:
803
+ await download_bytes_from_url("ftp://example.com/file")
804
+ assert exc_info.value.parameter == "url"
805
+ assert exc_info.value.value == "ftp"
806
807
808
@pytest.mark.asyncio
809
async def test_download_bytes_rejects_cloud_metadata_ip():
810
"""Cloud metadata endpoints must be blocked when an allowlist is set."""
811
url = "http://169.254.169.254/latest/meta-data/"
799
- with pytest.raises(ValueError, match="allowed domains"):
812
+ with pytest.raises(VLLMValidationError, match="allowed domains"):
813
await download_bytes_from_url(url, allowed_media_domains=["example.com"])
814
815
@@ -808,7 +821,7 @@ async def test_download_bytes_rejects_internal_ip():
821
"http://192.168.1.1/admin",
822
"http://127.0.0.1:8080/internal",
823
]:
811
- with pytest.raises(ValueError, match="allowed domains"):
824
+ with pytest.raises(VLLMValidationError, match="allowed domains"):
825
await download_bytes_from_url(
826
internal_url, allowed_media_domains=["example.com"]
827
)
@@ -850,17 +863,17 @@ async def test_download_bytes_no_allowlist_permits_any_domain():
863
async def test_download_bytes_empty_allowlist_denies_all():
864
"""An empty allowlist must deny all HTTP URLs (least privilege)."""
865
url = "https://any-domain.example.org/file.wav"
853
- with pytest.raises(ValueError, match="allowed domains"):
866
+ with pytest.raises(VLLMValidationError, match="allowed domains"):
867
await download_bytes_from_url(url, allowed_media_domains=[])
868
869
870
@pytest.mark.asyncio
871
async def test_download_bytes_unsupported_scheme():
872
"""Unsupported URL schemes must be rejected regardless of allowlist."""
860
- with pytest.raises(ValueError, match="Unsupported URL scheme"):
873
+ with pytest.raises(VLLMValidationError, match="Unsupported URL scheme"):
874
await download_bytes_from_url("ftp://example.com/file.wav")
875
863
- with pytest.raises(ValueError, match="Unsupported URL scheme"):
876
+ with pytest.raises(VLLMValidationError, match="Unsupported URL scheme"):
877
await download_bytes_from_url(
878
"ftp://example.com/file.wav",
879
allowed_media_domains=["example.com"],
@@ -875,7 +888,7 @@ async def test_download_bytes_backslash_bypass():
888
The fix normalizes through urllib3 before handing to aiohttp.
889
"""
890
bypass_url = "http://allowed.example.com\\@evil.internal/secret"
878
- with pytest.raises(ValueError, match="allowed domains"):
891
+ with pytest.raises(VLLMValidationError, match="allowed domains"):
892
await download_bytes_from_url(
893
bypass_url, allowed_media_domains=["evil.internal"]
894
)
vllm/entrypoints/openai/run_batch.py
+16
-6
@@ -476,19 +476,27 @@ async def download_bytes_from_url(
476
if "base64" in header:
477
return base64.b64decode(data)
478
else:
479
- raise ValueError(f"Unsupported data URL encoding: {header}")
479
+ raise VLLMValidationError(
480
+ f"Unsupported data URL encoding: {header}",
481
+ parameter="url",
482
+ )
483
else:
481
- raise ValueError(f"Invalid data URL format: {url}")
484
+ raise VLLMValidationError(
485
+ f"Invalid data URL format: {url}",
486
+ parameter="url",
487
+ )
488
489
# Handle HTTP/HTTPS URLs
490
elif parsed.scheme in ("http", "https"):
491
if allowed_media_domains is not None:
492
url_spec = parse_url(url)
493
if url_spec.hostname not in allowed_media_domains:
488
- raise ValueError(
494
+ raise VLLMValidationError(
495
f"The URL must be from one of the allowed domains: "
496
f"{allowed_media_domains}. Input URL domain: "
491
- f"{url_spec.hostname}"
497
+ f"{url_spec.hostname}",
498
+ parameter="url",
499
+ value=url_spec.hostname,
500
)
501
# Use the normalized URL to prevent parsing discrepancies
502
# between urllib3 and aiohttp (e.g. backslash-@ attacks).
@@ -499,9 +507,11 @@ async def download_bytes_from_url(
507
)
508
509
else:
502
- raise ValueError(
510
+ raise VLLMValidationError(
511
f"Unsupported URL scheme: {parsed.scheme}. "
504
- "Supported schemes: http, https, data"
512
+ "Supported schemes: http, https, data",
513
+ parameter="url",
514
+ value=parsed.scheme,
515
)
516
517