From 170e1ed936b197dbe8fb94d8a5e1e4451850a36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Sun, 20 Sep 2026 12:09:22 +0200 Subject: [PATCH] fix: keep the Error object an exception is built from SCIMException.from_error() rebuilt the status and the scimType from the exception class, so everything it has no class for was lost: a 429 status came back as 400, and a vendor specific scimType came back empty. to_error() now gives back the object it was built from. A client turning a server error into an exception and back into an HTTP response no longer changes the status the server sent. --- doc/changelog.rst | 11 +++++++++++ scim2_models/exceptions.py | 23 +++++++++++++++++++++-- tests/test_exceptions.py | 22 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 59f3dcb9..1a25d2da 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,17 @@ Changelog ========= +[Unreleased] +------------ + +Fixed +^^^^^ +- :meth:`~scim2_models.SCIMException.from_error` keeps the :class:`~scim2_models.Error` + object it is built from, and :meth:`~scim2_models.SCIMException.to_error` gives it back. + The status and the scimType a server sent used to be replaced by the ones of the + exception class, which dropped everything it has no class for, such as a ``429`` status + or a vendor specific scimType. + [0.8.0] - 2026-09-20 -------------------- diff --git a/scim2_models/exceptions.py b/scim2_models/exceptions.py index c374be27..3627bfd0 100644 --- a/scim2_models/exceptions.py +++ b/scim2_models/exceptions.py @@ -39,6 +39,7 @@ def __init__( self.context = context self._detail = detail self.scim_ctx = scim_ctx + self._error: Error | None = None super().__init__(detail or self._default_detail) @property @@ -47,9 +48,16 @@ def detail(self) -> str: return self._detail or self._default_detail def to_error(self) -> "Error": - """Convert this exception to a SCIM Error response object.""" + """Convert this exception to a SCIM Error response object. + + Exceptions built by :meth:`~scim2_models.SCIMException.from_error` return + the very object they were built from. + """ from .messages.error import Error + if self._error is not None: + return self._error + return Error( status=self.status, scim_type=self.scim_type or None, @@ -70,6 +78,11 @@ def from_error( ) -> "SCIMException": """Create an exception from a SCIM Error object. + The error object is kept as-is, and + :meth:`~scim2_models.SCIMException.to_error` gives it back. This matters for + the values no exception class describes, such as a ``429`` status or a + vendor specific scimType. + :param error: The SCIM Error object to convert. :param scim_ctx: The SCIM context in which the exception occurred. :return: The appropriate SCIMException subclass instance. @@ -80,7 +93,13 @@ def from_error( raise TypeError(f"Expected Error, got {type(error).__name__}") exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls) - return exception_class(detail=error.detail, scim_ctx=scim_ctx) + exc = exception_class(detail=error.detail, scim_ctx=scim_ctx) + exc._error = error + if error.status is not None: + exc.status = error.status + if error.scim_type: + exc.scim_type = error.scim_type + return exc class InvalidFilterException(SCIMException): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 2287da86..4fbda8ad 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -471,3 +471,25 @@ def test_from_error_without_scim_ctx(): exc = SCIMException.from_error(error) assert isinstance(exc, InvalidFilterException) assert exc.scim_ctx is None + + +def test_from_error_keeps_the_error_object(): + """to_error() gives back the very object from_error() was built from.""" + error = Error(status=404, detail="Resource unknown not found") + exc = SCIMException.from_error(error) + assert exc.to_error() is error + + +def test_from_error_keeps_what_no_exception_class_describes(): + """from_error() keeps a status and a scimType that match no exception class.""" + error = Error(status=429, scim_type="tooManyRequests", detail="Slow down") + exc = SCIMException.from_error(error) + assert exc.status == 429 + assert exc.scim_type == "tooManyRequests" + + +def test_from_error_without_status(): + """from_error() keeps the class status when the Error object carries none.""" + error = Error(scim_type="uniqueness", detail="Duplicate userName") + exc = SCIMException.from_error(error) + assert exc.status == 409