Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions airflow-ctl/src/airflowctl/ctl/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
37 changes: 37 additions & 0 deletions airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,43 @@ def raise_error(_args):

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()
Expand Down