Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Make Stripe errors serializable
pr_url: https://github.com/stripe/stripe-python/pull/1944
semver_level: patch
jira_tickets_closed:
- RUN_DEVSDK-3443
github_issues_resolved:
- https://github.com/stripe/stripe-python/issues/1941
---

Stripe exceptions can now be serialized and deserialized with Python `pickle` and Celery JSON result serialization without losing their type or attributes.
30 changes: 28 additions & 2 deletions stripe/_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ def __init__(
headers: Optional[Dict[str, str]] = None,
code: Optional[str] = None,
):
super(StripeError, self).__init__(message)

body: Optional[str] = None
if http_body:
# http_body can sometimes be a memoryview which must be cast
Expand All @@ -46,6 +44,14 @@ def __init__(
self.json_body = json_body
self.headers = headers or {}
self.code = code
super(StripeError, self).__init__(
message,
self.http_body,
self.http_status,
self.json_body,
self.headers,
self.code,
)
self.request_id = self.headers.get("request-id", None)
self.error = self._construct_error_object()

Expand Down Expand Up @@ -113,6 +119,7 @@ def __init__(
message, http_body, http_status, json_body, headers, code
)
self.should_retry = should_retry
self.args = self.args + (self.should_retry,)


class StripeErrorWithParamCode(StripeError):
Expand Down Expand Up @@ -146,6 +153,15 @@ def __init__(
message, http_body, http_status, json_body, headers, code
)
self.param = param
self.args = (
self._message,
self.param,
self.code,
self.http_body,
self.http_status,
self.json_body,
self.headers,
)


class IdempotencyError(StripeError):
Expand All @@ -167,6 +183,15 @@ def __init__(
message, http_body, http_status, json_body, headers, code
)
self.param = param
self.args = (
self._message,
self.param,
self.code,
self.http_body,
self.http_status,
self.json_body,
self.headers,
)


class AuthenticationError(StripeError):
Expand All @@ -185,6 +210,7 @@ class SignatureVerificationError(StripeError):
def __init__(self, message, sig_header, http_body=None):
super(SignatureVerificationError, self).__init__(message, http_body)
self.sig_header = sig_header
self.args = (self._message, self.sig_header, self.http_body)


# classDefinitions: The beginning of the section generated from our OpenAPI spec
Expand Down
8 changes: 8 additions & 0 deletions stripe/oauth_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ def __init__(
super(OAuthError, self).__init__(
description, http_body, http_status, json_body, headers, code
)
self.args = (
self.code,
self._message,
self.http_body,
self.http_status,
self.json_body,
self.headers,
)

def _construct_error_object(self):
if self.json_body is None:
Expand Down
96 changes: 94 additions & 2 deletions tests/test_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,37 @@


import json

from stripe._error import StripeError, CardError, APIConnectionError
import pickle

import pytest

from stripe._error import (
APIConnectionError,
APIError,
AuthenticationError,
CardError,
IdempotencyError,
InvalidRequestError,
PermissionError,
RateLimitError,
SignatureVerificationError,
StripeError,
StripeErrorWithParamCode,
TemporarySessionExpiredError,
)


def assert_error_round_trips(err):
json_args = json.loads(json.dumps(err.args))
round_tripped_errors = (
pickle.loads(pickle.dumps(err)),
type(err)(*json_args),
)

for round_tripped in round_tripped_errors:
assert type(round_tripped) is type(err)
assert round_tripped.args == err.args
assert vars(round_tripped) == vars(err)


class TestStripeError(object):
Expand Down Expand Up @@ -114,3 +143,66 @@ def test_default_no_retry(self):

err = APIConnectionError("msg", should_retry=True)
assert err.should_retry


@pytest.mark.parametrize(
"error_class",
[
StripeError,
APIError,
AuthenticationError,
IdempotencyError,
PermissionError,
RateLimitError,
StripeErrorWithParamCode,
TemporarySessionExpiredError,
],
)
def test_stripe_error_round_trips(error_class):
err = error_class(
message="message",
http_body=b"body",
http_status=400,
json_body={"error": {"code": "error_code"}},
headers={"request-id": "req_123"},
code="error_code",
)

assert_error_round_trips(err)


@pytest.mark.parametrize(
"err",
[
APIConnectionError(
"message",
b"body",
400,
{"error": {"code": "error_code"}},
{"request-id": "req_123"},
"error_code",
True,
),
CardError(
"message",
"param",
"error_code",
b"body",
402,
{"error": {"code": "error_code"}},
{"request-id": "req_123"},
),
InvalidRequestError(
message="message",
param="param",
code="error_code",
http_body=b"body",
http_status=400,
json_body={"error": {"code": "error_code"}},
headers={"request-id": "req_123"},
),
SignatureVerificationError("message", "signature", b"body"),
],
)
def test_stripe_error_with_custom_constructor_round_trips(err):
assert_error_round_trips(err)
37 changes: 37 additions & 0 deletions tests/test_oauth_error.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# -*- coding: utf-8 -*-


import json
import pickle

import pytest

from stripe import oauth_error


Expand All @@ -11,3 +16,35 @@ def test_error_object(self):
)
assert err.error is not None
assert err.error.error == "some_oauth_error"


@pytest.mark.parametrize(
"error_class",
[
oauth_error.OAuthError,
oauth_error.InvalidClientError,
oauth_error.InvalidGrantError,
oauth_error.InvalidRequestError,
oauth_error.InvalidScopeError,
oauth_error.UnsupportedGrantTypeError,
oauth_error.UnsupportedResponseTypeError,
],
)
def test_oauth_error_round_trips(error_class):
err = error_class(
code="oauth_code",
description="description",
http_body=b"body",
http_status=400,
json_body={"error": "oauth_code"},
headers={"request-id": "req_123"},
)
json_args = json.loads(json.dumps(err.args))

for round_tripped in (
pickle.loads(pickle.dumps(err)),
error_class(*json_args),
):
assert type(round_tripped) is error_class
assert round_tripped.args == err.args
assert vars(round_tripped) == vars(err)
Loading