From 3c1adb7b5fe989dea4f4cac6290db50a18cc67d3 Mon Sep 17 00:00:00 2001 From: adhavan18 Date: Thu, 10 Sep 2026 19:38:12 +0530 Subject: [PATCH] fix(streaming): validate SSE retry field per spec instead of leniently int()-ing it int() accepts -1, +1000, leading/trailing whitespace, and other forms the SSE spec doesn't allow for the retry field, only ASCII digits are valid. retry: -1 was silently accepted as a negative reconnection time instead of being ignored. Validate with value.isascii() and value.isdigit() before assigning, matching the spec's [0-9]+ grammar exactly. --- src/openai/_streaming.py | 7 ++++--- tests/test_streaming.py | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/openai/_streaming.py b/src/openai/_streaming.py index bb45a70f6a..f635a841eb 100644 --- a/src/openai/_streaming.py +++ b/src/openai/_streaming.py @@ -419,10 +419,11 @@ def decode(self, line: str) -> ServerSentEvent | None: else: self._last_event_id = value elif fieldname == "retry": - try: + # Per the SSE spec, a retry field is valid only if it consists entirely + # of ASCII digits; anything else (a sign, whitespace, a decimal point) + # must be ignored rather than parsed leniently by int(). + if value.isascii() and value.isdigit(): self._retry = int(value) - except (TypeError, ValueError): - pass else: pass # Field is ignored. diff --git a/tests/test_streaming.py b/tests/test_streaming.py index fe74fcc558..e87ab206e3 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -9,7 +9,7 @@ import pytest from openai import OpenAI, AsyncOpenAI, APITimeoutError, APIConnectionError -from openai._streaming import Stream, AsyncStream, ServerSentEvent +from openai._streaming import SSEDecoder, Stream, AsyncStream, ServerSentEvent @pytest.fixture( @@ -408,6 +408,27 @@ def body() -> Iterator[bytes]: assert response.is_closed +@pytest.mark.parametrize("value", ["-1", "+1000", "1.5", " 100", "100 ", "1e3", ""]) +def test_sse_decoder_ignores_invalid_retry_value(value: str) -> None: + decoder = SSEDecoder() + decoder.decode(f"retry: {value}") + decoder.decode("data: ok") + sse = decoder.decode("") + + assert sse is not None + assert sse.retry is None + + +def test_sse_decoder_accepts_valid_retry_value() -> None: + decoder = SSEDecoder() + decoder.decode("retry: 3000") + decoder.decode("data: ok") + sse = decoder.decode("") + + assert sse is not None + assert sse.retry == 3000 + + async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: for chunk in iter: yield chunk