Skip to content

Commit b472fc0

Browse files
committed
Adding application response error handling.
1 parent eabf204 commit b472fc0

File tree

5 files changed

+234
-5
lines changed

5 files changed

+234
-5
lines changed

intercom/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
22

33
from datetime import datetime
4-
from .errors import (ArgumentError, HttpError, IntercomError, # noqa
5-
ResourceNotFound, AuthenticationError, ServerError, BadGatewayError,
6-
ServiceUnavailableError) # noqa
4+
from .errors import (ArgumentError, AuthenticationError, # noqa
5+
BadGatewayError, BadRequestError, HttpError, IntercomError,
6+
MultipleMatchingUsersError, RateLimitExceeded, ResourceNotFound,
7+
ServerError, ServiceUnavailableError, UnexpectedError)
78
from .lib.setter_property import SetterProperty
89
from .request import Request
910
from .admin import Admin # noqa

intercom/errors.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ class HttpError(Exception):
1010

1111

1212
class IntercomError(Exception):
13-
pass
13+
14+
def __init__(self, message=None, context=None):
15+
super(IntercomError, self).__init__(message)
16+
self.context = context
1417

1518

1619
class ResourceNotFound(IntercomError):
@@ -31,3 +34,32 @@ class BadGatewayError(IntercomError):
3134

3235
class ServiceUnavailableError(IntercomError):
3336
pass
37+
38+
39+
class BadRequestError(IntercomError):
40+
pass
41+
42+
43+
class RateLimitExceeded(IntercomError):
44+
pass
45+
46+
47+
class MultipleMatchingUsersError(IntercomError):
48+
pass
49+
50+
51+
class UnexpectedError(IntercomError):
52+
pass
53+
54+
55+
error_codes = {
56+
'unauthorized': AuthenticationError,
57+
'forbidden': AuthenticationError,
58+
'bad_request': BadRequestError,
59+
'missing_parameter': BadRequestError,
60+
'parameter_invalid': BadRequestError,
61+
'not_found': ResourceNotFound,
62+
'rate_limit_exceeded': RateLimitExceeded,
63+
'service_unavailable': ServiceUnavailableError,
64+
'conflict': MultipleMatchingUsersError,
65+
}

intercom/request.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@ def send_request_to_path(cls, method, url, auth, params=None):
3535
cls.raise_errors_on_failure(resp)
3636

3737
if resp.content:
38-
return json.loads(resp.content)
38+
return cls.parse_body(resp)
39+
40+
@classmethod
41+
def parse_body(cls, resp):
42+
try:
43+
body = json.loads(resp.content)
44+
except ValueError:
45+
cls.raise_errors_on_failure(resp)
46+
if body.get('type') == 'error.list':
47+
cls.raise_application_errors_on_failure(body, resp.status_code)
48+
return body
3949

4050
@classmethod
4151
def raise_errors_on_failure(cls, resp):
@@ -52,6 +62,42 @@ def raise_errors_on_failure(cls, resp):
5262
elif resp.status_code == 503:
5363
raise errors.ServiceUnavailableError('Service Unavailable')
5464

65+
@classmethod
66+
def raise_application_errors_on_failure(cls, error_list_details, http_code): # noqa
67+
# Currently, we don't support multiple errors
68+
error_details = error_list_details['errors'][0]
69+
error_code = error_details.get('type')
70+
if error_code is None:
71+
error_code = error_details.get('code')
72+
error_context = {
73+
'http_code': http_code,
74+
'application_error_code': error_code
75+
}
76+
error_class = errors.error_codes.get(error_code)
77+
if error_class is None:
78+
# unexpected error
79+
if error_code:
80+
message = cls.message_for_unexpected_error_with_type(
81+
error_details, http_code)
82+
else:
83+
message = cls.message_for_unexpected_error_without_type(
84+
error_details, http_code)
85+
error_class = errors.UnexpectedError
86+
else:
87+
message = error_details['message']
88+
raise error_class(message, error_context)
89+
90+
@classmethod
91+
def message_for_unexpected_error_with_type(cls, error_details, http_code): # noqa
92+
error_type = error_details['type']
93+
message = error_details['message']
94+
return "The error of type '%s' is not recognized. It occurred with the message: %s and http_code: '%s'. Please contact Intercom with these details." % (error_type, message, http_code) # noqa
95+
96+
@classmethod
97+
def message_for_unexpected_error_without_type(cls, error_details, http_code): # noqa
98+
message = error_details['message']
99+
return "An unexpected error occured. It occurred with the message: %s and http_code: '%s'. Please contact Intercom with these details." % (message, http_code) # noqa
100+
55101

56102
class ResourceEncoder(json.JSONEncoder):
57103
def default(self, o):

tests/unit/request_spec.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import httpretty
44
import intercom
5+
import json
56
import re
67
from describe import expect
78
from intercom import Intercom
9+
from intercom import UnexpectedError
810

911
get = httpretty.GET
1012
post = httpretty.POST
@@ -54,3 +56,134 @@ def it_raises_service_unavailable_error(self):
5456
get, r(r'/notes$'), body='', status=503)
5557
with expect.to_raise_error(intercom.ServiceUnavailableError):
5658
Intercom.get('/notes')
59+
60+
@httpretty.activate
61+
def it_raises_an_unexpected_typed_error(self):
62+
payload = {
63+
'type': 'error.list',
64+
'errors': [
65+
{
66+
'type': 'hopper',
67+
'message': 'The first compiler.'
68+
}
69+
]
70+
}
71+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
72+
try:
73+
Intercom.get('/users')
74+
except (UnexpectedError) as err:
75+
assert "The error of type 'hopper' is not recognized" in err.message # noqa
76+
expect(err.context['http_code']) == 200
77+
expect(err.context['application_error_code']) == 'hopper'
78+
79+
@httpretty.activate
80+
def it_raises_an_unexpected_untyped_error(self):
81+
payload = {
82+
'type': 'error.list',
83+
'errors': [
84+
{
85+
'message': 'UNIVAC'
86+
}
87+
]
88+
}
89+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
90+
try:
91+
Intercom.get('/users')
92+
except (UnexpectedError) as err:
93+
assert "An unexpected error occured." in err.message
94+
expect(err.context['application_error_code']) is None
95+
96+
@httpretty.activate
97+
def it_raises_a_bad_request_error(self):
98+
payload = {
99+
'type': 'error.list',
100+
'errors': [
101+
{
102+
'type': None,
103+
'message': 'email is required'
104+
}
105+
]
106+
}
107+
108+
for code in ['missing_parameter', 'parameter_invalid', 'bad_request']:
109+
payload['errors'][0]['type'] = code
110+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
111+
with expect.to_raise_error(intercom.BadRequestError):
112+
Intercom.get('/users')
113+
114+
@httpretty.activate
115+
def it_raises_an_authentication_error(self):
116+
payload = {
117+
'type': 'error.list',
118+
'errors': [
119+
{
120+
'type': 'unauthorized',
121+
'message': 'Your name\'s not down.'
122+
}
123+
]
124+
}
125+
for code in ['unauthorized', 'forbidden']:
126+
payload['errors'][0]['type'] = code
127+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
128+
with expect.to_raise_error(intercom.AuthenticationError):
129+
Intercom.get('/users')
130+
131+
@httpretty.activate
132+
def it_raises_resource_not_found_by_type(self):
133+
payload = {
134+
'type': 'error.list',
135+
'errors': [
136+
{
137+
'type': 'not_found',
138+
'message': 'Waaaaally?'
139+
}
140+
]
141+
}
142+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
143+
with expect.to_raise_error(intercom.ResourceNotFound):
144+
Intercom.get('/users')
145+
146+
@httpretty.activate
147+
def it_raises_rate_limit_exceeded(self):
148+
payload = {
149+
'type': 'error.list',
150+
'errors': [
151+
{
152+
'type': 'rate_limit_exceeded',
153+
'message': 'Fair use please.'
154+
}
155+
]
156+
}
157+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
158+
with expect.to_raise_error(intercom.RateLimitExceeded):
159+
Intercom.get('/users')
160+
161+
@httpretty.activate
162+
def it_raises_a_service_unavailable_error(self):
163+
payload = {
164+
'type': 'error.list',
165+
'errors': [
166+
{
167+
'type': 'service_unavailable',
168+
'message': 'Zzzzz.'
169+
}
170+
]
171+
}
172+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
173+
with expect.to_raise_error(intercom.ServiceUnavailableError):
174+
Intercom.get('/users')
175+
176+
@httpretty.activate
177+
def it_raises_a_multiple_matching_users_error(self):
178+
payload = {
179+
'type': 'error.list',
180+
'errors': [
181+
{
182+
'type': 'conflict',
183+
'message': 'Two many cooks.'
184+
}
185+
]
186+
}
187+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
188+
with expect.to_raise_error(intercom.MultipleMatchingUsersError):
189+
Intercom.get('/users')

tests/unit/user_spec.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from describe import expect
1111
from intercom.collection_proxy import CollectionProxy
1212
from intercom.lib.flat_store import FlatStore
13+
from intercom import Intercom
1314
from intercom import User
15+
from intercom import MultipleMatchingUsersError
1416
from intercom.utils import create_class_instance
1517
from tests.unit import test_user
1618

@@ -317,6 +319,21 @@ def it_returns_the_total_number_of_users(self):
317319
mock_count.return_value = 100
318320
expect(100) == User.count()
319321

322+
@httpretty.activate
323+
def it_raises_a_multiple_matching_users_error_when_receiving_a_conflict(self): # noqa
324+
payload = {
325+
'type': 'error.list',
326+
'errors': [
327+
{
328+
'code': 'conflict',
329+
'message': 'Multiple existing users match this email address - must be more specific using user_id' # noqa
330+
}
331+
]
332+
}
333+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
334+
with expect.to_raise_error(MultipleMatchingUsersError):
335+
Intercom.get('/users')
336+
320337
class DescribeIncrementingCustomAttributeFields:
321338

322339
def before_each(self, context):

0 commit comments

Comments
 (0)