[Bugfix] Respect media redirect policy for image references (#6122)

Signed-off-by: HaningZS <resossr0v0@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
Zhenghan SongandHongsheng Liu authored and GitHub committed 2026-08-17 13:25:09 +08:00
1 parent 65b3d41b3a
commit 309d9c3ea8
4 files changed
+169 -3

No files matched your search

+8
View File
@@ -167,6 +167,14 @@ transfer options such as `control_path`, `control_guidance`,
`num_conditional_frames`, `show_control_condition`, and `show_input`; see the
Cosmos3 recipe for complete examples.
HTTP redirects for `image_reference.image_url` follow vLLM's
`VLLM_MEDIA_URL_ALLOW_REDIRECTS` setting. Before starting the server, set it to
`1` (the default) to allow redirects or `0` to reject them. A redirect target
can differ from the original host, and vLLM-Omni does not yet fully implement
upstream vLLM's media URL allowlist protection. Deployments should therefore
treat remote media URLs as untrusted and choose this setting as part of their
URL access policy.
### Speech-to-Video
For models that support audio-driven generation (e.g., Wan2.2-S2V), pass both
@@ -2,16 +2,122 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for OpenAI-compatible video API encoding helpers."""
import base64
from io import BytesIO
import httpx
import numpy as np
import pytest
import torch
from PIL import Image
from vllm import envs
from vllm_omni.diffusion.postprocess import rife_interpolator
from vllm_omni.entrypoints.openai import video_api_utils
from vllm_omni.entrypoints.openai.errors import InvalidInputReferenceError
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _png_bytes() -> bytes:
buffer = BytesIO()
Image.new("RGB", (2, 1), color=(12, 34, 56)).save(buffer, format="PNG")
return buffer.getvalue()
def _install_http_transport(monkeypatch, handler, client_kwargs):
async_client = httpx.AsyncClient
def _client_factory(*args, **kwargs):
client_kwargs.append(kwargs.copy())
return async_client(*args, transport=httpx.MockTransport(handler), **kwargs)
monkeypatch.setattr(video_api_utils.httpx, "AsyncClient", _client_factory)
@pytest.mark.asyncio
async def test_decode_image_url_follows_redirects_when_allowed(monkeypatch):
requested_paths = []
client_kwargs = []
def _handler(request):
requested_paths.append(request.url.path)
if request.url.path == "/redirect.png":
return httpx.Response(302, headers={"location": "/image.png"})
return httpx.Response(200, content=_png_bytes(), headers={"content-type": "image/png"})
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
_install_http_transport(monkeypatch, _handler, client_kwargs)
image = await video_api_utils.decode_image_url("https://example.com/redirect.png")
assert image.size == (2, 1)
assert image.mode == "RGB"
assert requested_paths == ["/redirect.png", "/image.png"]
assert client_kwargs == [{"timeout": 60, "follow_redirects": True}]
@pytest.mark.asyncio
async def test_decode_image_url_rejects_redirects_when_disabled(monkeypatch):
requested_paths = []
client_kwargs = []
def _handler(request):
requested_paths.append(request.url.path)
return httpx.Response(302, headers={"location": "/image.png"})
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", False)
_install_http_transport(monkeypatch, _handler, client_kwargs)
with pytest.raises(InvalidInputReferenceError, match="redirect.*VLLM_MEDIA_URL_ALLOW_REDIRECTS"):
await video_api_utils.decode_image_url("https://example.com/redirect.png")
assert requested_paths == ["/redirect.png"]
assert client_kwargs == [{"timeout": 60, "follow_redirects": False}]
@pytest.mark.asyncio
async def test_decode_image_url_reports_http_status(monkeypatch):
client_kwargs = []
def _handler(request):
return httpx.Response(404)
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
_install_http_transport(monkeypatch, _handler, client_kwargs)
with pytest.raises(InvalidInputReferenceError, match="server returned HTTP 404"):
await video_api_utils.decode_image_url("https://example.com/missing.png")
@pytest.mark.asyncio
async def test_decode_image_url_reports_connection_failure(monkeypatch):
client_kwargs = []
def _handler(request):
raise httpx.ConnectError("connection refused", request=request)
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
_install_http_transport(monkeypatch, _handler, client_kwargs)
with pytest.raises(InvalidInputReferenceError, match="failed to download image"):
await video_api_utils.decode_image_url("https://example.com/unreachable.png")
@pytest.mark.asyncio
async def test_decode_image_url_keeps_data_urls_local(monkeypatch):
def _unexpected_http_client(*args, **kwargs):
pytest.fail("data URLs must not create an HTTP client")
monkeypatch.setattr(video_api_utils.httpx, "AsyncClient", _unexpected_http_client)
encoded_image = base64.b64encode(_png_bytes()).decode()
image = await video_api_utils.decode_image_url(f"data:image/png;base64,{encoded_image}")
assert image.size == (2, 1)
assert image.mode == "RGB"
def _install_fake_video_mux(monkeypatch, mux_calls):
def _fake_mux_video_audio_bytes(frames, audio, fps, audio_sample_rate, video_codec_options=None):
mux_calls.append(
@@ -16,15 +16,17 @@ from pathlib import Path
from types import SimpleNamespace
import av
import httpx
import numpy as np
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from PIL import Image
from pytest_mock import MockerFixture
from vllm import envs
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
from vllm_omni.entrypoints.openai import api_server
from vllm_omni.entrypoints.openai import api_server, video_api_utils
from vllm_omni.entrypoints.openai.api_server import router
from vllm_omni.entrypoints.openai.protocol.videos import (
VideoGenerationRequest,
@@ -599,6 +601,45 @@ def test_i2v_video_generation_with_image_reference_form(test_client, mocker: Moc
assert input_image.size == (40, 24)
def test_i2v_video_generation_follows_allowed_image_redirect(test_client, mocker: MockerFixture, monkeypatch):
requested_paths = []
async_client = httpx.AsyncClient
def _handler(request):
requested_paths.append(request.url.path)
if request.url.path == "/redirect.png":
return httpx.Response(302, headers={"location": "/image.png"})
return httpx.Response(200, content=_make_test_image_bytes((40, 24)))
def _client_factory(*args, **kwargs):
return async_client(*args, transport=httpx.MockTransport(_handler), **kwargs)
monkeypatch.setattr(envs, "VLLM_MEDIA_URL_ALLOW_REDIRECTS", True)
monkeypatch.setattr(video_api_utils.httpx, "AsyncClient", _client_factory)
mocker.patch(
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
return_value=b"fake-video",
)
response = test_client.post(
"/v1/videos",
data={
"prompt": "A fox running through snow.",
"image_reference": json.dumps({"image_url": "https://example.com/redirect.png"}),
},
)
assert response.status_code == 200
video_id = response.json()["id"]
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
assert requested_paths == ["/redirect.png", "/image.png"]
engine = test_client.app.state.openai_serving_video._engine_client
input_image = engine.captured_prompt["multi_modal_data"]["image"]
assert isinstance(input_image, Image.Image)
assert input_image.size == (40, 24)
def test_v2v_video_generation_form(test_client, mocker: MockerFixture):
video_bytes = _make_test_video_bytes((32, 24), num_frames=3)
@@ -17,6 +17,7 @@ import httpx
import numpy as np
import torch
from PIL import Image, UnidentifiedImageError
from vllm import envs
from vllm.multimodal.video import (
VIDEO_LOADER_REGISTRY,
VideoBackend,
@@ -172,11 +173,21 @@ async def decode_image_url(image_url: str) -> Image.Image:
return _decode_base64_image(image_url, source="image_reference.image_url")
if image_url.startswith(("http://", "https://")):
async with httpx.AsyncClient(timeout=60) as client:
allow_redirects = envs.VLLM_MEDIA_URL_ALLOW_REDIRECTS
async with httpx.AsyncClient(timeout=60, follow_redirects=allow_redirects) as client:
try:
response = await client.get(image_url)
response.raise_for_status()
except httpx.HTTPError as exc:
except httpx.HTTPStatusError as exc:
if exc.response.has_redirect_location and not allow_redirects:
raise InvalidInputReferenceError(
"Invalid image_reference.image_url: redirect response was rejected because "
"VLLM_MEDIA_URL_ALLOW_REDIRECTS is disabled."
) from exc
raise InvalidInputReferenceError(
f"Invalid image_reference.image_url: server returned HTTP {exc.response.status_code}."
) from exc
except httpx.RequestError as exc:
raise InvalidInputReferenceError(
"Invalid image_reference.image_url: failed to download image."
) from exc