This is an automated email from the ASF dual-hosted git repository.
henry3260 pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this
push:
new 61d3710166f [airflow-ctl/v0-1-test] Fix airflowctl showing a traceback
instead of the server error on 5xx (#70841) (#70910)
61d3710166f is described below
commit 61d3710166f102c5f17962e78855505fcee6b00c
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Aug 2 09:06:16 2026 +0800
[airflow-ctl/v0-1-test] Fix airflowctl showing a traceback instead of the
server error on 5xx (#70841) (#70910)
(cherry picked from commit eef1b970acd833e2841a87cbea61bab555f8fbf7)
Co-authored-by: rjgoyln <[email protected]>
---
airflow-ctl/src/airflowctl/api/operations.py | 22 ++++++------
airflow-ctl/tests/airflow_ctl/api/test_client.py | 46 ++++++++++++++++++++++++
2 files changed, 56 insertions(+), 12 deletions(-)
diff --git a/airflow-ctl/src/airflowctl/api/operations.py
b/airflow-ctl/src/airflowctl/api/operations.py
index 0a08a8fa2c3..bbfc18fce2c 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -110,18 +110,16 @@ class ServerResponseError(httpx.HTTPStatusError):
if response.headers.get("content-type") != "application/json":
return None
- if 400 <= response.status_code < 500:
- response.read()
- return cls(
- message=f"Client error message: {response.json()}",
- request=response.request,
- response=response,
- )
-
- msg = response.json()
-
- self = cls(message=msg, request=response.request, response=response)
- return self
+ # httpx runs response event hooks before it reads the body, so the
body has to be
+ # pulled in explicitly here or ``.json()`` raises
``httpx.ResponseNotRead``.
+ response.read()
+
+ error_kind = "Client" if response.status_code < 500 else "Server"
+ return cls(
+ message=f"{error_kind} error message: {response.json()}",
+ request=response.request,
+ response=response,
+ )
def _check_flag_and_exit_if_server_response_error(func):
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index 3ce4466efef..dc33092e780 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -53,6 +53,22 @@ def make_client_w_responses(responses: list[httpx.Response])
-> Client:
return Client(base_url="", token="", mounts={"'http://":
httpx.MockTransport(handle_request)})
+def make_unread_json_response(status_code: int, payload: dict, **kwargs) ->
httpx.Response:
+ """
+ Build a JSON response whose body has not been read yet.
+
+ ``httpx.Response(json=...)`` eagerly loads the body, which hides the fact
that response
+ event hooks run before httpx reads a real server's body. Passing an
iterator keeps the
+ body streaming, matching what the hooks see against a live API server.
+ """
+ return httpx.Response(
+ status_code,
+ headers={"content-type": "application/json"},
+ content=iter([json.dumps(payload).encode()]),
+ **kwargs,
+ )
+
+
@pytest.fixture(autouse=True)
def unique_config_dir():
temp_dir = tempfile.mkdtemp()
@@ -107,6 +123,23 @@ class TestClient:
client.get("http://error")
assert err.value.args == ("Client error message: {'detail': 'Not
found'}",)
+ @pytest.mark.parametrize(
+ ("status_code", "expected_message"),
+ [
+ pytest.param(404, "Client error message: {'detail': 'boom'}",
id="client-error"),
+ pytest.param(500, "Server error message: {'detail': 'boom'}",
id="server-error"),
+ ],
+ )
+ def test_error_parsing_with_unread_body(self, status_code,
expected_message):
+ response = make_unread_json_response(
+ status_code, {"detail": "boom"}, request=httpx.Request("GET",
"http://error")
+ )
+
+ with pytest.raises(ServerResponseError) as err:
+ get_json_error(response)
+
+ assert err.value.args == (expected_message,)
+
@pytest.mark.parametrize(
("suppress_error_log", "expected_warning_count"),
[
@@ -407,6 +440,19 @@ class TestSaveKeyringPatching:
assert response.status_code == 200
assert len(responses) == 1
+ def test_retry_handling_server_error_with_unread_body(self):
+ with time_machine.travel("2023-01-01T00:00:00Z", tick=False):
+ responses: list[httpx.Response] = [
+ make_unread_json_response(500, {"detail": "boom"}),
+ httpx.Response(200, json={"detail": "Recovered from error"}),
+ httpx.Response(400, json={"detail": "Should not get here"}),
+ ]
+ client = make_client_w_responses(responses)
+
+ response = client.get("http://error")
+ assert response.status_code == 200
+ assert len(responses) == 1
+
def test_retry_handling_non_retry_error(self):
with time_machine.travel("2023-01-01T00:00:00Z", tick=False):
responses: list[httpx.Response] = [