[Bugfix] Accept RFC 2397 parameters in base64 data URLs (#48973)

Signed-off-by: Thomas Fahrner <thomas.fahrner@parasail.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Thomas Fahrner committed Jul 24, 2026 at 08:23 UTC d02df748bf9efd99022f1a062597dc3cb3808485
2 files changed +47 -5
tests/multimodal/media/test_connector.py
+38
@@ -225,6 +225,44 @@ async def test_fetch_image_local_files_with_space_in_name(image_url: str):
225 assert not ImageChops.difference(image_sync, image_async).getbbox()
226
227
228 +@pytest.mark.asyncio
229 +async def test_fetch_image_data_url_with_params():
230 + """RFC 2397 allows parameters between the mediatype and the base64
231 + marker; they must not be rejected or leak into the media type."""
232 + connector = MediaConnector()
233 +
234 + image = Image.new("RGB", (4, 4), color=(255, 0, 0))
235 + with NamedTemporaryFile(suffix=".png") as f:
236 + image.save(f.name)
237 + base64_image = base64.b64encode(f.read()).decode("utf-8")
238 +
239 + data_url = f"data:image/png;charset=utf-8;base64,{base64_image}"
240 + image_sync = connector.fetch_image(data_url)
241 + image_async = await connector.fetch_image_async(data_url)
242 + assert _image_equals(image_sync, image_async)
243 +
244 +
245 +def test_fetch_image_data_url_malformed():
246 + connector = MediaConnector()
247 +
248 + with pytest.raises(ValueError, match="missing ','"):
249 + connector.fetch_image("data:image/png;base64")
250 +
251 + with pytest.raises(NotImplementedError, match="base64"):
252 + connector.fetch_image("data:text/plain,hello")
253 +
254 + # ";base64" requires the ";"; here "base64" is a (bogus) media type.
255 + with pytest.raises(NotImplementedError, match="base64"):
256 + connector.fetch_image("data:base64,aGVsbG8=")
257 +
258 + # Strict RFC 2397 grammar: lowercase "base64", no whitespace.
259 + with pytest.raises(NotImplementedError, match="base64"):
260 + connector.fetch_image("data:image/png;BASE64,aGVsbG8=")
261 +
262 + with pytest.raises(NotImplementedError, match="base64"):
263 + connector.fetch_image("data:image/png; base64,aGVsbG8=")
264 +
265 +
266 @pytest.mark.asyncio
267 async def test_fetch_image_error_conversion():
268 connector = MediaConnector()
vllm/multimodal/media/connector.py
+9 -5
@@ -302,14 +302,18 @@ class MediaConnector:
302 media_io: MediaIO[_M],
303 ) -> _M: # type: ignore[type-var]
304 # Format per RFC 2397:
305 - # data:[<mediatype>][;base64],<data>
306 - data_spec, data = url[5:].split(",", 1)
307 - media_type, data_type = data_spec.split(";", 1)
308 -
309 - if data_type != "base64":
305 + # data:[<mediatype>][;<param>=<value>]*[;base64],<data>
306 + data_spec, sep, data = url[5:].partition(",")
307 + if not sep:
308 + msg = f"Invalid data URL {url[:32]!r}: missing ',' separator."
309 + raise ValueError(msg)
310 +
311 + media_type, sep, encoding = data_spec.rpartition(";")
312 + if not sep or encoding != "base64":
313 msg = "Only base64 data URLs are supported for now."
314 raise NotImplementedError(msg)
315
316 + media_type = media_type.partition(";")[0]
317 return media_io.load_base64(media_type, data)
318
319 def _load_file_url(