[Bugfix][Frontend] Fix run_batch upload retrying on success and unawaited error body (#50588)

Signed-off-by: Chao Tan <chaos.tc@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>

Tan Chao committed Aug 24, 2026 at 04:33 UTC e239947777e18071c8053195ce599b6511717f67
2 files changed +64 -3
tests/entrypoints/openai/test_run_batch.py
+58
@@ -20,6 +20,7 @@ from vllm.entrypoints.openai.run_batch import (
20 BatchTranscriptionRequest,
21 download_bytes_from_url,
22 make_transcription_wrapper,
23 + upload_data,
24 )
25 from vllm.exceptions import VLLMValidationError
26 from vllm.utils.mem_constants import MiB_bytes
@@ -1052,3 +1053,60 @@ async def test_transcription_wrapper_rejects_oversized_http_before_handler(
1053 assert isinstance(response, ErrorResponse)
1054 assert "Maximum file size exceeded" in response.error.message
1055 handler.assert_not_awaited()
1056 +
1057 +
1058 +# ---------------------------------------------------------------------------
1059 +# Unit tests for upload_data retry behavior
1060 +# ---------------------------------------------------------------------------
1061 +
1062 +
1063 +def _make_aiohttp_put_session(status: int = 200, body_text: str = ""):
1064 + """Mock an aiohttp.ClientSession whose PUT returns the given status."""
1065 + mock_resp = MagicMock()
1066 + mock_resp.status = status
1067 + mock_resp.text = AsyncMock(return_value=body_text)
1068 + mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
1069 + mock_resp.__aexit__ = AsyncMock(return_value=False)
1070 +
1071 + mock_session = MagicMock()
1072 + mock_session.put = MagicMock(return_value=mock_resp)
1073 + mock_session.__aenter__ = AsyncMock(return_value=mock_session)
1074 + mock_session.__aexit__ = AsyncMock(return_value=False)
1075 + return mock_session
1076 +
1077 +
1078 +@pytest.mark.asyncio
1079 +async def test_upload_data_uploads_once_on_success():
1080 + """A successful upload must not be retried (regression guard)."""
1081 + session = _make_aiohttp_put_session(status=200)
1082 + with patch(
1083 + "vllm.entrypoints.openai.run_batch.aiohttp.ClientSession",
1084 + return_value=session,
1085 + ):
1086 + await upload_data(
1087 + "https://example.com/output.jsonl", "payload", from_file=False
1088 + )
1089 + assert session.put.call_count == 1
1090 +
1091 +
1092 +@pytest.mark.asyncio
1093 +async def test_upload_data_error_includes_awaited_response_body():
1094 + """A failed upload must surface the awaited response body, not a coroutine."""
1095 + session = _make_aiohttp_put_session(status=500, body_text="server-error-detail")
1096 + with (
1097 + patch(
1098 + "vllm.entrypoints.openai.run_batch.aiohttp.ClientSession",
1099 + return_value=session,
1100 + ),
1101 + patch(
1102 + "vllm.entrypoints.openai.run_batch.asyncio.sleep",
1103 + AsyncMock(),
1104 + ),
1105 + pytest.raises(Exception) as exc_info,
1106 + ):
1107 + await upload_data(
1108 + "https://example.com/output.jsonl", "payload", from_file=False
1109 + )
1110 + message = str(exc_info.value)
1111 + assert "server-error-detail" in message
1112 + assert "coroutine" not in message
vllm/entrypoints/openai/run_batch.py
+6 -3
@@ -393,20 +393,23 @@ async def upload_data(output_url: str, data_or_file: str, from_file: bool) -> No
393 with open(data_or_file, "rb") as file:
394 async with session.put(output_url, data=file) as response:
395 if response.status != 200:
396 + error_text = await response.text()
397 raise Exception(
398 f"Failed to upload file.\n"
399 f"Status: {response.status}\n"
399 - f"Response: {response.text()}"
400 + f"Response: {error_text}"
401 )
402 else:
403 async with session.put(output_url, data=data_or_file) as response:
404 if response.status != 200:
405 + error_text = await response.text()
406 raise Exception(
407 f"Failed to upload data.\n"
408 f"Status: {response.status}\n"
407 - f"Response: {response.text()}"
409 + f"Response: {error_text}"
410 )
409 -
411 + # Upload succeeded; do not retry.
412 + return
413 except Exception as e:
414 if attempt < max_retries:
415 logger.error(