Fix image URL loading crashes and add a download timeout - #308
Conversation
image_from_chunk raised an IndexError on data URLs without a comma, treated any URL starting with "file" (e.g. "file.png") as a local file path, and leaked the file handle it opened. download_image called requests.get without a timeout, so a hanging server blocked the encode path forever.
juliendenize
left a comment
There was a problem hiding this comment.
Thanks for your contribution it makes a lot of sense.
My concern is that I think timeout should be configurable by the environment with a nice error message to inform user they can increase it.
|
|
||
|
|
||
| def download_image(url: str) -> Image.Image: | ||
| def download_image(url: str, timeout: float = 10.0) -> Image.Image: |
There was a problem hiding this comment.
Can we not make it a default here ?
Serving stacks cannot pass timeout= into encode_chat_completion. Read MISTRAL_COMMON_IMAGE_DOWNLOAD_TIMEOUT (default 10s), pass it explicitly from image_from_chunk, and name both knobs if the download times out.
Thanks so much for your review @juliendenize. I think your recommendations make sense, I have implemented them in the most recent commit.
On timeout the error is: So a heavy image pipeline can raise the cap without threading timeout through the tokenizer. |
juliendenize
left a comment
There was a problem hiding this comment.
Small follow-up to our earlier discussion: image_from_chunk resolves the timeout and download_image resolves it again when timeout=None — two resolution paths for one value. These suggestions keep the resolution in exactly one place (download_image's timeout is None branch) and update the test to guard that image_from_chunk doesn't override the default resolution.
Also flagging: the latest commit (dcc54b7) leaves image_from_chunk in the tests/test_image.py import block with a tab instead of spaces and no trailing comma, which is a syntax error — pytest tests/test_image.py fails at collection on this branch. Suggestion attached.
Verified locally on top of this branch: pytest tests/test_image.py → 51 passed; ruff check + ruff format --check clean.
juliendenize
left a comment
There was a problem hiding this comment.
Small follow-up to our earlier discussion: image_from_chunk resolves the timeout and download_image resolves it again when timeout=None — two resolution paths for one value. These suggestions keep the resolution in exactly one place (download_image's timeout is None branch) and update the test to guard that image_from_chunk doesn't override the default resolution.
Also flagging: the latest commit (dcc54b7) leaves image_from_chunk in the tests/test_image.py import block with a tab instead of spaces and no trailing comma, which is a syntax error — pytest tests/test_image.py fails at collection on this branch. Suggestion attached.
Verified locally on top of this branch: pytest tests/test_image.py → 51 passed; ruff check + ruff format --check clean.
Co-authored-by: Julien Denize <40604584+juliendenize@users.noreply.github.com>
|
Hmm sorry about the comment it's generated obviously, i'm just fixing some stuff related to style / minor refactoring and then i'll merge. Thanks a lot for iterating and the contrib ! |
f7bd87b to
ca1a36f
Compare
No problem at all, thanks for merging! |
Transformers' test suite patches mistral_common download_image with transformers.image_utils.load_image(image, timeout=None), which breaks on the url=/timeout= keyword-only call added in #308. Call positionally and default timeout to None so both signatures work. Co-authored-by: juliendenize <juliendenize@users.noreply.github.com>
Transformers' test suite patches mistral_common download_image with transformers.image_utils.load_image(image, timeout=None), which breaks on the url=/timeout= keyword-only call added in #308. Call positionally and default timeout to None so both signatures work. Co-authored-by: juliendenize <juliendenize@users.noreply.github.com>
Summary
image_from_chunkmishandles three of the four URL shapes it accepts, and the HTTP branch can hang forever.Deterministic:
This function is on the public encode path:
ImageEncoder.__call__->InstructTokenizerV3._encode_content_chunk->MistralTokenizer.encode_chat_completion, which is what vLLM and the TransformersMistralCommonBackendcall.ImageURLChunkcontent comes from the request, so all three inputs are attacker-supplied.Fixes #307
Real world example
A serving stack runs the experimental tokenize server (
mistral_common.experimental.app) or any framework that callsencode_chat_completionon user requests. A client sends:{"messages": [{"role": "user", "content": [ {"type": "image_url", "image_url": {"url": "data:image/png;base64"}} ]}]}What happens today:
image_from_chunksplits on ","IndexError: list index out of rangeValueError;IndexErrorescapes as an unhandled 500"url": "https://attacker.example/slow"requests.getnever returns; the encode worker is gone until restartThe
startswith("file")arm is quieter: a URL likefile.pngis opened relative to the server's working directory, so a name collision reads a local file instead of raising "Unsupported image url scheme". The handle is also never closed.After this fix: the malformed data URL and the bare
file.pngraiseRuntimeError(the same family the function already uses for unsupported schemes), and the HTTP branch gives up after 10 seconds with the existing "Error downloading the image" wrapping, sincerequests.exceptions.Timeoutis aRequestException.Fix
partition; raiseRuntimeErrorwhen there is no payload (covers both the missing comma and the empty payload).file://prefix for the local-file branch and open the file in awithblock (Image.load()before close), so bare names fall through to the unsupported-scheme error and the handle is closed. Behavior note: multi-frame images (GIF/TIFF) can no longer be seeked past frame 0 after return; the encoder only ever used frame 0, and the old code merely leaked the fd that made seeking possible.timeout(default 10.0s) fromdownload_imagetorequests.get. Exposed as a parameter so callers can tune it; existing error wrapping is unchanged.Not a duplicate
#289 and #294 fixed crashes in image sizing/config after loading; this PR is about loading itself. #259 touches audio data URL prefixes, different file.
Audio.from_urlhas the same missing timeout but a different grammar, so it stays out of scope here; flagged in the issue as follow-up.Test
No existing test covered malformed data URLs, bare file names, or the timeout (the two existing mocks accept any call signature). Added:
test_image_from_chunk_data_url_without_payload(both;base64and;base64,)test_image_from_chunk_bare_file_name_is_unsupportedtest_image_from_chunk_file_uri(regression guard for realfile://URIs)test_download_image_passes_timeout(assertstimeout=reachesrequests.get, and that aTimeoutbecomesRuntimeError)The first, second and fourth fail on
mainand pass with this change:Full unit suite:
uv run pytest tests/ --ignore=tests/integrations --ignore=tests/integration -n 4 --dist loadfile-> 1233 passed, 16 skipped. Doctests, ruff check, ruff format and mypy all pass on the changed files.Verification
main)RuntimeError;requests.getreceivestimeout=10.0file://URIs and mocked HTTP downloads still encode byte-identically (existingtest_download_image,test_image_encoder_formatsuntouched apart from mock signatures accepting the new kwarg)download_imageupdated with the newtimeoutarg; no other user-facing API change