From 1baf372bfc14d739860b4a7122877e22ef0dcbf1 Mon Sep 17 00:00:00 2001 From: Tan Chao Date: Fri, 21 Aug 2026 09:07:24 -0700 Subject: [PATCH] [Frontend] Use VLLMValidationError for batch request URL validation (#50191) Signed-off-by: Chao Tan --- tests/entrypoints/openai/test_run_batch.py | 27 ++++++++++++++++------ vllm/entrypoints/openai/run_batch.py | 22 +++++++++++++----- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/tests/entrypoints/openai/test_run_batch.py b/tests/entrypoints/openai/test_run_batch.py index 0f7d7f5f46..b2f01cfd7e 100644 --- a/tests/entrypoints/openai/test_run_batch.py +++ b/tests/entrypoints/openai/test_run_batch.py @@ -13,6 +13,7 @@ from vllm.entrypoints.openai.run_batch import ( BatchRequestOutput, download_bytes_from_url, ) +from vllm.exceptions import VLLMValidationError CHAT_MODEL_NAME = "hmellor/tiny-random-LlamaForCausalLM" EMBEDDING_MODEL_NAME = "intfloat/multilingual-e5-small" @@ -788,15 +789,27 @@ async def test_download_bytes_data_url_bypasses_domain_check(): async def test_download_bytes_rejects_disallowed_domain(): """HTTP URLs whose hostname is not in the allowlist must be rejected.""" url = "https://evil.internal/secret" - with pytest.raises(ValueError, match="allowed domains"): + with pytest.raises(VLLMValidationError, match="allowed domains") as exc_info: await download_bytes_from_url(url, allowed_media_domains=["example.com"]) + # URL validation failures carry structured metadata for the frontend. + assert exc_info.value.parameter == "url" + assert exc_info.value.value == "evil.internal" + + +@pytest.mark.asyncio +async def test_download_bytes_rejects_unsupported_scheme(): + """Unsupported URL schemes are rejected with structured metadata.""" + with pytest.raises(VLLMValidationError, match="Unsupported URL scheme") as exc_info: + await download_bytes_from_url("ftp://example.com/file") + assert exc_info.value.parameter == "url" + assert exc_info.value.value == "ftp" @pytest.mark.asyncio async def test_download_bytes_rejects_cloud_metadata_ip(): """Cloud metadata endpoints must be blocked when an allowlist is set.""" url = "http://169.254.169.254/latest/meta-data/" - with pytest.raises(ValueError, match="allowed domains"): + with pytest.raises(VLLMValidationError, match="allowed domains"): await download_bytes_from_url(url, allowed_media_domains=["example.com"]) @@ -808,7 +821,7 @@ async def test_download_bytes_rejects_internal_ip(): "http://192.168.1.1/admin", "http://127.0.0.1:8080/internal", ]: - with pytest.raises(ValueError, match="allowed domains"): + with pytest.raises(VLLMValidationError, match="allowed domains"): await download_bytes_from_url( internal_url, allowed_media_domains=["example.com"] ) @@ -850,17 +863,17 @@ async def test_download_bytes_no_allowlist_permits_any_domain(): async def test_download_bytes_empty_allowlist_denies_all(): """An empty allowlist must deny all HTTP URLs (least privilege).""" url = "https://any-domain.example.org/file.wav" - with pytest.raises(ValueError, match="allowed domains"): + with pytest.raises(VLLMValidationError, match="allowed domains"): await download_bytes_from_url(url, allowed_media_domains=[]) @pytest.mark.asyncio async def test_download_bytes_unsupported_scheme(): """Unsupported URL schemes must be rejected regardless of allowlist.""" - with pytest.raises(ValueError, match="Unsupported URL scheme"): + with pytest.raises(VLLMValidationError, match="Unsupported URL scheme"): await download_bytes_from_url("ftp://example.com/file.wav") - with pytest.raises(ValueError, match="Unsupported URL scheme"): + with pytest.raises(VLLMValidationError, match="Unsupported URL scheme"): await download_bytes_from_url( "ftp://example.com/file.wav", allowed_media_domains=["example.com"], @@ -875,7 +888,7 @@ async def test_download_bytes_backslash_bypass(): The fix normalizes through urllib3 before handing to aiohttp. """ bypass_url = "http://allowed.example.com\\@evil.internal/secret" - with pytest.raises(ValueError, match="allowed domains"): + with pytest.raises(VLLMValidationError, match="allowed domains"): await download_bytes_from_url( bypass_url, allowed_media_domains=["evil.internal"] ) diff --git a/vllm/entrypoints/openai/run_batch.py b/vllm/entrypoints/openai/run_batch.py index f4b7b35bf0..a367791764 100644 --- a/vllm/entrypoints/openai/run_batch.py +++ b/vllm/entrypoints/openai/run_batch.py @@ -476,19 +476,27 @@ async def download_bytes_from_url( if "base64" in header: return base64.b64decode(data) else: - raise ValueError(f"Unsupported data URL encoding: {header}") + raise VLLMValidationError( + f"Unsupported data URL encoding: {header}", + parameter="url", + ) else: - raise ValueError(f"Invalid data URL format: {url}") + raise VLLMValidationError( + f"Invalid data URL format: {url}", + parameter="url", + ) # Handle HTTP/HTTPS URLs elif parsed.scheme in ("http", "https"): if allowed_media_domains is not None: url_spec = parse_url(url) if url_spec.hostname not in allowed_media_domains: - raise ValueError( + raise VLLMValidationError( f"The URL must be from one of the allowed domains: " f"{allowed_media_domains}. Input URL domain: " - f"{url_spec.hostname}" + f"{url_spec.hostname}", + parameter="url", + value=url_spec.hostname, ) # Use the normalized URL to prevent parsing discrepancies # between urllib3 and aiohttp (e.g. backslash-@ attacks). @@ -499,9 +507,11 @@ async def download_bytes_from_url( ) else: - raise ValueError( + raise VLLMValidationError( f"Unsupported URL scheme: {parsed.scheme}. " - "Supported schemes: http, https, data" + "Supported schemes: http, https, data", + parameter="url", + value=parsed.scheme, )