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
22 changes: 21 additions & 1 deletion src/httpware/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
from httpware._internal.redaction import redact_url


_EXCEPTION_ARGS: types.GetSetDescriptorType = BaseException.__dict__["args"]


def _restore_client_error(cls: "type[ClientError]", args: tuple[Any, ...]) -> "ClientError":
return cls.__new__(cls, *args)

Expand Down Expand Up @@ -71,7 +74,24 @@ class StatusError(ClientError):

def __init__(self, response: httpx2.Response) -> None:
self.response = response
super().__init__(self._summary())

@property
def args(self) -> tuple[Any, ...]:
"""The one-element ``(message,)`` tuple, the message built on first access."""
args = _EXCEPTION_ARGS.__get__(self)
# BaseException.__new__ stored the constructor's (response,) here.
if len(args) == 1 and args[0] is vars(self).get("response"):
args = (self._summary(),)
_EXCEPTION_ARGS.__set__(self, args)
return args

@args.setter
def args(self, value: tuple[Any, ...]) -> None:
_EXCEPTION_ARGS.__set__(self, value)

def __str__(self) -> str:
args = self.args
return str(args[0]) if len(args) == 1 else super().__str__()

def _summary(self) -> str:
method = self.response.request.method
Expand Down
23 changes: 23 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,29 @@ def test_status_error_summary_message_includes_status_method_url() -> None:
assert str(exc) == "404 GET https://example.test/missing"


def test_status_error_args_hold_the_summary_message() -> None:
exc = NotFoundError(_make_response(404, url="https://user:pass@example.test/x?token=s", method="GET"))
assert exc.args == ("404 GET https://example.test/x?token=REDACTED",)


def test_status_error_builds_its_message_on_first_use(monkeypatch: pytest.MonkeyPatch) -> None:
redacted: list[str] = []
monkeypatch.setattr(httpware.errors, "redact_url", redacted.append)
NotFoundError(_make_response(404))
assert redacted == []


@pytest.mark.parametrize(
("args", "message"),
[(("custom",), "custom"), (("a", "b"), "('a', 'b')"), ((), "")],
)
def test_status_error_assigned_args_replace_the_message(args: tuple[str, ...], message: str) -> None:
exc = NotFoundError(_make_response(404))
exc.args = args
assert exc.args == args
assert str(exc) == message


def test_status_error_strips_userinfo_in_summary_message() -> None:
exc = NotFoundError(_make_response(404, url="https://user:pass@example.test/x"))
assert "user" not in str(exc)
Expand Down
Loading