This is an automated email from the ASF dual-hosted git repository.
potiuk 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 fa6e87843f6 [airflow-ctl/v0-1-test] Fix airflowctl tracebacks on
redirects and non-JSON error bodies (#73307) (#73331)
fa6e87843f6 is described below
commit fa6e87843f687b68610a62ddeb2686b4f0cf473c
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Sep 22 02:15:02 2026 +0200
[airflow-ctl/v0-1-test] Fix airflowctl tracebacks on redirects and non-JSON
error bodies (#73307) (#73331)
airflowctl funnels every command through a single handler whose job is to
turn a failure into one line the operator can act on. That handler only
recognised ServerResponseError, which the API client builds solely for
4xx/5xx responses carrying a JSON body -- everything else leaves the
response hook raising the bare httpx.HTTPStatusError that
ServerResponseError subclasses, so no clause caught it.
The responses that fall outside that narrow definition are precisely the
ones an operator has least context for: an SSO proxy answering with a
redirect, or an ingress returning its own HTML error page. Those never
reached the Airflow API server at all, and a traceback tells the operator
nothing about why.
(cherry picked from commit 44094b2661b7f90d3cb0f53d1a43a457bb38a979)
Co-authored-by: Y-C <[email protected]>
---
airflow-ctl/src/airflowctl/ctl/cli_config.py | 11 +++++++
.../tests/airflow_ctl/ctl/test_cli_config.py | 37 ++++++++++++++++++++++
2 files changed, 48 insertions(+)
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 8306393a556..e19a3fd62d6 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -106,6 +106,17 @@ def safe_call_command(function: Callable, args:
Iterable[Arg]) -> None:
"If you need help, run the command with --help."
)
sys.exit(1)
+ # Must stay below ``ServerResponseError``, which subclasses it. Responses
the client could not
+ # turn into a ``ServerResponseError`` -- a 3xx, or a 4xx/5xx whose body is
not JSON -- reach us
+ # as the bare httpx error.
+ except httpx.HTTPStatusError as e:
+ rich.print(f"[red]Server response error: {e}[/red]")
+ if e.response.is_redirect:
+ rich.print(
+ "[red]The server answered with a redirect, which airflowctl
does not follow. "
+ "Please check that the API URL you logged in with points at
the Airflow API server.[/red]"
+ )
+ sys.exit(1)
class DefaultHelpParser(argparse.ArgumentParser):
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
index 281bd2b040b..8d358c47830 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -513,6 +513,43 @@ class TestCliConfigMethods:
assert ctx.value.code == 1
+ @pytest.mark.parametrize(
+ ("response", "hint_expected"),
+ [
+ pytest.param(
+ httpx.Response(302, headers={"location":
"https://sso.example.com/login"}),
+ True,
+ id="redirect",
+ ),
+ pytest.param(
+ httpx.Response(502, headers={"content-type": "text/html"},
content=b"<html>nope</html>"),
+ False,
+ id="non-json-server-error",
+ ),
+ pytest.param(
+ httpx.Response(401, headers={"content-type": "text/html"},
content=b"<html>nope</html>"),
+ False,
+ id="non-json-client-error",
+ ),
+ ],
+ )
+ def test_safe_call_command_exits_non_zero_for_bare_http_status_error(
+ self, response, hint_expected, capsys
+ ):
+ response.request = httpx.Request("GET",
"http://localhost:8080/api/v2/dags")
+
+ def raise_error(_args):
+ response.raise_for_status()
+
+ with pytest.raises(SystemExit) as ctx:
+ safe_call_command(raise_error, args=argparse.Namespace())
+
+ assert ctx.value.code == 1
+ # Rich hard-wraps at the console width, so normalise before matching
on a phrase.
+ out = " ".join(capsys.readouterr().out.split())
+ assert "Server response error:" in out
+ assert ("does not follow" in out) is hint_expected
+
def test_add_to_parser_drops_type_for_boolean_optional_action(self):
"""Test add_to_parser removes type for BooleanOptionalAction."""
parser = argparse.ArgumentParser()