Skip to content

Commit 170e1ed

Browse files
committed
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.
1 parent 9cf98fa commit 170e1ed

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

‎doc/changelog.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changelog
22
=========
33

4+
[Unreleased]
5+
------------
6+
7+
Fixed
8+
^^^^^
9+
- :meth:`~scim2_models.SCIMException.from_error` keeps the :class:`~scim2_models.Error`
10+
object it is built from, and :meth:`~scim2_models.SCIMException.to_error` gives it back.
11+
The status and the scimType a server sent used to be replaced by the ones of the
12+
exception class, which dropped everything it has no class for, such as a ``429`` status
13+
or a vendor specific scimType.
14+
415
[0.8.0] - 2026-09-20
516
--------------------
617

‎scim2_models/exceptions.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
self.context = context
4040
self._detail = detail
4141
self.scim_ctx = scim_ctx
42+
self._error: Error | None = None
4243
super().__init__(detail or self._default_detail)
4344

4445
@property
@@ -47,9 +48,16 @@ def detail(self) -> str:
4748
return self._detail or self._default_detail
4849

4950
def to_error(self) -> "Error":
50-
"""Convert this exception to a SCIM Error response object."""
51+
"""Convert this exception to a SCIM Error response object.
52+
53+
Exceptions built by :meth:`~scim2_models.SCIMException.from_error` return
54+
the very object they were built from.
55+
"""
5156
from .messages.error import Error
5257

58+
if self._error is not None:
59+
return self._error
60+
5361
return Error(
5462
status=self.status,
5563
scim_type=self.scim_type or None,
@@ -70,6 +78,11 @@ def from_error(
7078
) -> "SCIMException":
7179
"""Create an exception from a SCIM Error object.
7280
81+
The error object is kept as-is, and
82+
:meth:`~scim2_models.SCIMException.to_error` gives it back. This matters for
83+
the values no exception class describes, such as a ``429`` status or a
84+
vendor specific scimType.
85+
7386
:param error: The SCIM Error object to convert.
7487
:param scim_ctx: The SCIM context in which the exception occurred.
7588
:return: The appropriate SCIMException subclass instance.
@@ -80,7 +93,13 @@ def from_error(
8093
raise TypeError(f"Expected Error, got {type(error).__name__}")
8194

8295
exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls)
83-
return exception_class(detail=error.detail, scim_ctx=scim_ctx)
96+
exc = exception_class(detail=error.detail, scim_ctx=scim_ctx)
97+
exc._error = error
98+
if error.status is not None:
99+
exc.status = error.status
100+
if error.scim_type:
101+
exc.scim_type = error.scim_type
102+
return exc
84103

85104

86105
class InvalidFilterException(SCIMException):

‎tests/test_exceptions.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,25 @@ def test_from_error_without_scim_ctx():
471471
exc = SCIMException.from_error(error)
472472
assert isinstance(exc, InvalidFilterException)
473473
assert exc.scim_ctx is None
474+
475+
476+
def test_from_error_keeps_the_error_object():
477+
"""to_error() gives back the very object from_error() was built from."""
478+
error = Error(status=404, detail="Resource unknown not found")
479+
exc = SCIMException.from_error(error)
480+
assert exc.to_error() is error
481+
482+
483+
def test_from_error_keeps_what_no_exception_class_describes():
484+
"""from_error() keeps a status and a scimType that match no exception class."""
485+
error = Error(status=429, scim_type="tooManyRequests", detail="Slow down")
486+
exc = SCIMException.from_error(error)
487+
assert exc.status == 429
488+
assert exc.scim_type == "tooManyRequests"
489+
490+
491+
def test_from_error_without_status():
492+
"""from_error() keeps the class status when the Error object carries none."""
493+
error = Error(scim_type="uniqueness", detail="Duplicate userName")
494+
exc = SCIMException.from_error(error)
495+
assert exc.status == 409

0 commit comments

Comments
 (0)