main
py 173 lines 6.09 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 """Tests for VLLMUnprocessableEntityError and media fetch error handling.
5
6 Verifies that unprocessable image URLs (404, 403, DNS failures, etc.) return
7 HTTP 422 instead of 500.
8 """
9
10 from http import HTTPStatus
11 from unittest.mock import AsyncMock, MagicMock, patch
12
13 import aiohttp
14 import pytest
15
16 from vllm.connections import MediaDownloadSizeExceededError
17 from vllm.entrypoints.serve import create_error_response
18 from vllm.exceptions import VLLMClientError, VLLMUnprocessableEntityError
19 from vllm.multimodal.media import MediaConnector
20
21
22 class TestVLLMUnprocessableEntityError:
23 """Tests for VLLMUnprocessableEntityError exception."""
24
25 def test_creation(self):
26 exc = VLLMUnprocessableEntityError("Test error")
27 assert str(exc) == "Test error"
28 assert exc.parameter is None
29
30 def test_creation_with_parameter_and_value(self):
31 exc = VLLMUnprocessableEntityError(
32 "Test error",
33 parameter="image_url",
34 value="https://example.com/image.jpg",
35 )
36 assert "parameter=image_url" in str(exc)
37 assert "value=https://example.com/image.jpg" in str(exc)
38
39 def test_is_client_error_subclass(self):
40 exc = VLLMUnprocessableEntityError("Test")
41 assert isinstance(exc, VLLMClientError)
42
43
44 class TestMediaConnectorErrorHandling:
45 """Tests for MediaConnector error handling."""
46
47 @pytest.mark.asyncio
48 async def test_fetch_image_async_404(self):
49 connector = MediaConnector()
50
51 with patch.object(
52 connector.connection, "async_get_bytes", new_callable=AsyncMock
53 ) as mock_get:
54 mock_get.side_effect = aiohttp.ClientResponseError(
55 request_info=MagicMock(),
56 history=(),
57 status=404,
58 message="Not Found",
59 )
60
61 with pytest.raises(VLLMUnprocessableEntityError) as exc_info:
62 await connector.fetch_image_async("https://example.com/missing.jpg")
63
64 assert exc_info.value.parameter == "image_url"
65
66 @pytest.mark.asyncio
67 async def test_fetch_image_async_dns_error(self):
68 """DNS errors are transient and should remain as-is for retry."""
69 connector = MediaConnector()
70
71 with patch.object(
72 connector.connection, "async_get_bytes", new_callable=AsyncMock
73 ) as mock_get:
74 mock_get.side_effect = aiohttp.ClientConnectorDNSError(
75 connection_key=MagicMock(),
76 os_error=MagicMock(),
77 )
78
79 with pytest.raises(aiohttp.ClientConnectorDNSError) as exc_info:
80 await connector.fetch_image_async(
81 "https://nonexistent.example/image.jpg"
82 )
83
84 assert isinstance(exc_info.value, aiohttp.ClientConnectorDNSError)
85
86 @pytest.mark.asyncio
87 async def test_fetch_image_async_500_preserved(self):
88 """5xx errors should remain as server errors."""
89 connector = MediaConnector()
90
91 with patch.object(
92 connector.connection, "async_get_bytes", new_callable=AsyncMock
93 ) as mock_get:
94 mock_get.side_effect = aiohttp.ClientResponseError(
95 request_info=MagicMock(),
96 history=(),
97 status=500,
98 message="Internal Server Error",
99 )
100
101 with pytest.raises(aiohttp.ClientResponseError) as exc_info:
102 await connector.fetch_image_async("https://example.com/image.jpg")
103
104 assert exc_info.value.status == 500
105
106 def test_fetch_image_404(self):
107 connector = MediaConnector()
108
109 with patch.object(
110 connector.connection, "get_bytes", new_callable=MagicMock
111 ) as mock_get:
112 mock_get.side_effect = aiohttp.ClientResponseError(
113 request_info=MagicMock(),
114 history=(),
115 status=404,
116 message="Not Found",
117 )
118
119 with pytest.raises(VLLMUnprocessableEntityError) as exc_info:
120 connector.fetch_image("https://example.com/missing.jpg")
121
122 assert exc_info.value.parameter == "image_url"
123
124 def test_fetch_image_connection_error(self):
125 """Connection errors are transient and should remain as-is for retry."""
126 connector = MediaConnector()
127
128 with patch.object(
129 connector.connection, "get_bytes", new_callable=MagicMock
130 ) as mock_get:
131 mock_get.side_effect = aiohttp.ClientConnectionError("Connection refused")
132
133 with pytest.raises(aiohttp.ClientConnectionError) as exc_info:
134 connector.fetch_image("https://example.com/image.jpg")
135
136 assert isinstance(exc_info.value, aiohttp.ClientConnectionError)
137
138 def test_fetch_image_download_size_limit_error(self):
139 connector = MediaConnector()
140
141 with patch.object(
142 connector.connection, "get_bytes", new_callable=MagicMock
143 ) as mock_get:
144 mock_get.side_effect = MediaDownloadSizeExceededError(256 * 1024 * 1024)
145
146 with pytest.raises(VLLMUnprocessableEntityError) as exc_info:
147 connector.fetch_image("https://example.com/image.jpg")
148
149 assert "VLLM_MAX_MEDIA_DOWNLOAD_SIZE_MB=256" in str(exc_info.value)
150
151
152 class TestErrorResponse:
153 """Tests for error response creation."""
154
155 def test_unprocessable_entity_returns_422(self):
156 exc = VLLMUnprocessableEntityError(
157 "Failed to fetch media from URL: Cannot connect",
158 parameter="image_url",
159 value="https://example.com/image.jpg",
160 )
161
162 response = create_error_response(exc)
163
164 assert response.error.code == HTTPStatus.UNPROCESSABLE_ENTITY.value
165 assert response.error.type == "UnprocessableEntityError"
166 assert response.error.param == "image_url"
167
168 def test_unprocessable_entity_message(self):
169 exc = VLLMUnprocessableEntityError("Test error message")
170 response = create_error_response(exc)
171
172 assert response.error.message == "Test error message"
173 assert response.error.code == 422