This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 44094b2661b Fix airflowctl tracebacks on redirects and non-JSON error
bodies (#73307)
44094b2661b is described below
commit 44094b2661b7f90d3cb0f53d1a43a457bb38a979
Author: Y-C <[email protected]>
AuthorDate: Sat Sep 19 00:37:24 2026 +0800
Fix airflowctl tracebacks on redirects and non-JSON error bodies (#73307)
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.
---
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 a273784127e..3379d4d0520 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 6cb2fb19192..45d55f08034 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -536,6 +536,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()