From 9a1eac78acfa327a5f27d49b3ea1c47264362941 Mon Sep 17 00:00:00 2001 From: Emanuel Date: Thu, 30 Apr 2026 14:30:53 -0300 Subject: [PATCH] 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"))