Skip to content
Draft
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
34 changes: 14 additions & 20 deletions conjure_python_client/_http/requests_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@ def __setstate__(self, state):
super().__setstate__(state) # type: ignore


def _reconstruct_conjure_http_error(cls, state):
obj = cls.__new__(cls)
args = state.pop("args", ())
obj.__dict__.update(state)
obj.args = args
return obj


class ConjureHTTPError(HTTPError):
"""An HTTPError from a Conjure Service with ``SerializableError``
attributes extracted from the response."""
Expand Down Expand Up @@ -295,30 +303,16 @@ def __init__(self, http_error: HTTPError) -> None:
)

def __copy__(self):
"""The fact that ConjureHTTPError is a BaseException but its __init__
has a different signature causes a subtle issue for shallow copying.
During copy.copy(), __init__ will be called with args defined by
BaseException.__reduce_, which corresponds to default __init__. Since
they're inconsistent, what http_error receives is actually message,
hence an error.

By defining a __copy__ method, we give instructions to the intepreter
on how to reconstruct a ConjureHTTPError instance. Alternatively, we
could also fix it by changing the _init__ signature of this class.
Although cleaner, unfortunately it will be a breaking change.
"""

# Create a shell object without calling __init__
new_obj = type(self).__new__(type(self))

for attr, value in self.__dict__.items():
setattr(new_obj, attr, value)

# Exception args are not actually a part of __dict__...
new_obj.__dict__.update(self.__dict__)
new_obj.args = self.args

return new_obj

def __reduce__(self):
state = self.__dict__.copy()
state["args"] = self.args
return (_reconstruct_conjure_http_error, (type(self), state))

@property
def cause(self) -> Optional[HTTPError]:
"""The wrapped ``HTTPError`` that was the direct cause of
Expand Down
20 changes: 20 additions & 0 deletions test/_http/test_requests_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,23 @@ def json(self):
copied_error = copy.copy(original_error)
assert type(original_error) is type(copied_error)
assert str(original_error) == str(copied_error)


def test_pickling_conjure_http_error():
from requests.models import Response, PreparedRequest

response = Response()
response.status_code = 500
response.headers["X-B3-TraceId"] = "test"
response._content = b'{"errorCode": "INTERNAL", "errorName": "Default:Internal", "errorInstanceId": "abc", "parameters": {}}'
request = PreparedRequest()
request.prepare_url("http://example.com", {})
request.prepare_method("GET")
response.request = request

http_error = HTTPError("HTTP 500 Server Error", response=response, request=request)
original_error = ConjureHTTPError(http_error)

unpickled_error = pickle.loads(pickle.dumps(original_error))
assert type(original_error) is type(unpickled_error)
assert str(original_error) == str(unpickled_error)