From 9a1eac78acfa327a5f27d49b3ea1c47264362941 Mon Sep 17 00:00:00 2001 From: Emanuel Date: Thu, 30 Apr 2026 14:30:53 -0300 Subject: [PATCH 1/3] Retry on HTTP 200 with empty response body When Trino returns HTTP 200 with an empty body under load, the client was crashing with a bare JSONDecodeError. Treat this as a transient condition and retry using the existing backoff mechanism, consistent with how 429/502/503/504 are handled. If retries are exhausted, raise TrinoConnectionError with a descriptive message. Fixes #596 --- tests/unit/test_client.py | 26 ++++++++++++++++++++++++++ trino/client.py | 7 +++++++ 2 files changed, 33 insertions(+) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index f011a54d..07b5b68d 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1094,6 +1094,32 @@ def test_429_error_retry(monkeypatch): assert post_retry.retry_count == 3 +def test_empty_200_response_retry(monkeypatch): + http_resp = TrinoRequest.http.Response() + http_resp.status_code = 200 + http_resp._content = b"" + + post_retry = RetryRecorder(result=http_resp) + monkeypatch.setattr(TrinoRequest.http.Session, "post", post_retry) + + get_retry = RetryRecorder(result=http_resp) + monkeypatch.setattr(TrinoRequest.http.Session, "get", get_retry) + + attempts = 3 + req = TrinoRequest( + host="coordinator", + port=8080, + client_session=ClientSession(user="test"), + max_attempts=attempts, + ) + + req.post("SELECT 1") + assert post_retry.retry_count == attempts + + req.get("URL") + assert get_retry.retry_count == attempts + + @pytest.mark.parametrize("status_code", [ 501 ]) diff --git a/trino/client.py b/trino/client.py index 97ef11ae..adf53766 100644 --- a/trino/client.py +++ b/trino/client.py @@ -641,6 +641,9 @@ def max_attempts(self, value: int) -> None: # need retry when there is no exception but the status code is 429, 502, 503, or 504 lambda response: getattr(response, "status_code", None) in (429, 502, 503, 504), + # need retry when the server returns 200 with an empty body (transient under load) + lambda response: getattr(response, "status_code", None) == 200 + and not getattr(response, "text", "").strip(), ), max_attempts=self._max_attempts, ) @@ -723,6 +726,10 @@ def process(self, http_response: Response) -> TrinoStatus: self.raise_response_error(http_response) http_response.encoding = "utf-8" + if not http_response.text.strip(): + raise exceptions.TrinoConnectionError( + "received empty response from server (status 200)" + ) response = json.loads(http_response.text) if "error" in response and response["error"]: raise self._process_error(response["error"], response.get("id")) From 25b14f34dd11e9d92e43571194d9c14e8f0709d9 Mon Sep 17 00:00:00 2001 From: Ashhar Hasan Date: Fri, 10 Jul 2026 23:33:09 +0530 Subject: [PATCH 2/3] empty for CI From 8f19c07568d65fc60067969dbdb1971faf8195c7 Mon Sep 17 00:00:00 2001 From: Emanuel Sales Date: Mon, 13 Jul 2026 12:58:48 -0300 Subject: [PATCH 3/3] Add unit test for empty 200 response error in process() Covers the TrinoConnectionError raised when the server returns a 200 status with an empty response body. --- tests/unit/test_client.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 07b5b68d..f16f6cbd 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -813,6 +813,28 @@ def test_trino_connection_error(monkeypatch, error_code, error_type, error_messa assert error_message in str(error) +def test_trino_process_empty_200_response_error(): + req = TrinoRequest( + host="coordinator", + port=8080, + client_session=ClientSession( + user="test", + source="test", + catalog="test", + schema="test", + properties={}, + ), + http_scheme="http", + ) + + http_resp = TrinoRequest.http.Response() + http_resp.status_code = 200 + http_resp._content = b"" + with pytest.raises(trino.exceptions.TrinoConnectionError) as error: + req.process(http_resp) + assert "received empty response from server (status 200)" in str(error.value) + + def test_extra_credential(mock_get_and_post): _, post = mock_get_and_post