Error handling in RESTful API's
Mon 30 May 2016 - PyCon US 2016 Open Spaces
John Roa
Technical team Lead
Error and exception handling
Two error handling approaches
Look Before You Leap
(LBYL)
Easier to Ask for Forgiveness than Permission
(EAFP)
Look Before You Leap
(LBYL)
Lots of if statements
You always check to see if something can be done before doing it
Look Before You Leap
(LBYL)
def print_object(some_object):
# Check if the object is printable...
if isinstance(some_object, str):
print(some_object)
elif isinstance(some_object, dict):
print(some_object)
elif isinstance(some_object, list):
print(some_object)
# 97 elifs later...
else:
print("unprintable object")
Easier to Ask for Forgiveness than Permission
(EAFP)
you just do the thing
If it turns out that wasn't possible, shrug "my bad", and deal with it.
Easier to Ask for Forgiveness than Permission
(EAFP)
try:
# try to execute some code here
except Exception:
# if the initial execution fails, execute this one
Easier to Ask for Forgiveness than Permission
(EAFP)
def print_object(some_object):
# Check if the object is printable...
try:
printable = str(some_object)
print(printable)
except TypeError:
print("unprintable object")
CONTEXT
WEB API
UI: web pages, mobile apps
HTTP
Web APIs
- programmatic interface
- one or more publicly exposed endpoints
- defined request-response message system
REST
REpresentational State Transfer
Is an style of software architecture for distributed software
REST
Not only about using GET, POST, PUT, DELETE methods
REST
Six Constraints:
1. Uniform Interface
2. Stateless Interactions
3. Cacheable
4. Client-Server
5. Layered System
6. Code on Demand
UI: web pages, mobile apps
HTTP
RESTful APIs
- programmatic interface
- one or more publicly exposed endpoints
- defined request-response message system
- Implements REST architecture principles
let's cut to the chase
Error handling in RESTful API's
do's and don'ts
don't
Forget/Avoid error handling
class LambdaView(View):
@staticmethod
def get(request, object_id):
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
DON'T! :'(
try / except pass
try:
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except Exception :
pass
don't
Pokemon exception handling
don't
Pokemon exception handling: "gotta catch 'em all"
try:
# try to do something
except Exception as e:
# Gotta catch 'em all!
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
do
class LambdaEditView(View):
@staticmethod
def get(request, object_id):
try:
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except LambdaFunction.DoesNotExist:
return HttpResponse("Lambda not found.", status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Error handling through multiple exceptions :)
don't
class LambdaEditView(View):
@staticmethod
def get(request, object_id):
try:
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except LambdaFunction.DoesNotExist:
return HttpResponse("Lambda not found.", status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Error handling ONLY through exceptions
do
class LambdaEditView(View):
@staticmethod
def get(request, object_id):
try:
token = request.GET.get('token')
if not token:
return HttpResponse('Invalid token.', status=status.HTTP_400_BAD_REQUEST)
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except LambdaFunction.DoesNotExist:
return HttpResponse("Lambda not found.", status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Error handling through exceptions and if's
do
Error handling through exceptions and if's
No golden hammer here
Make sure of understanding the concepts and apply them depending on your particular case.
don't
Use a 200 status code for every response
HTTP/1.1 200 OK
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"success": false,
"data": {
"name": "Not Found Exception",
"message": "The requested resource was not found."
}
}
do
HTTP/1.1 404 Not Found
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"name": "Not Found Exception",
"message": "The requested resource was not found."
}
don't
Using a 500 status code for all errors
HTTP/1.1 500: Internal server error
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"success": false,
"data": {
"name": "Invalid parameter",
"message": "The given parameter is not valid."
}
}
do
HTTP/1.1 400: Bad request
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"success": false,
"data": {
"name": "Invalid parameter",
"message": "The given parameter is not valid."
}
}
How do I choose the code?
How do I choose the code?
1xx: Informational - Request received, continuing process
How do I choose the code?
2xx: Success - The action was successfully received, understood, and accepted
How do I choose the code?
3xx: Redirection - Further action must be taken in order to complete the request
How do I choose the code?
4xx: Client Error - The request contains bad syntax or cannot be fulfilled
How do I choose the code?
4xx: Client Error - The request contains bad syntax or cannot be fulfilled
How do I choose the code?
5xx: Server Error - The server failed to fulfill an apparently valid request
What?? that's a lot of codes!!
It's a matter of being practical...
Be practical ;)
2xx Success
200 OK
201 Created
204 No Content
3xx Redirection
304 Not Modified
4xx Client Error
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
5xx Server Error
500 Internal Server Error
Use the most common HTTP status codes
don't
Skip logging
class LambdaEditView(View):
@staticmethod
def get(request, object_id):
try:
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except LambdaFunction.DoesNotExist:
# Where is the log? :(
return HttpResponse("Lambda not found.", status=status.HTTP_404_NOT_FOUND)
except Exception as e:
# Where is the log? :(
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
don't
Logging 400-level codes at ‘error’ level
class LambdaEditView(View):
@staticmethod
def get(request, object_id):
try:
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except LambdaFunction.DoesNotExist:
log.error("Lambda with id %s was not found." % object_id)
return HttpResponse("Lambda not found.", status=status.HTTP_404_NOT_FOUND)
except Exception as e:
log.error("Unexpected error. Error detail: %s" % str(e))
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
do
Logging 400-level codes at ‘info’ level
class LambdaEditView(View):
@staticmethod
def get(request, object_id):
try:
...
...
return HttpResponse(LambdaFunction.objects.get(id=object_id).name)
except LambdaFunction.DoesNotExist:
log.info("Lambda with id %s was not found." % object_id)
return HttpResponse("Lambda not found.", status=status.HTTP_404_NOT_FOUND)
except Exception as e:
log.error("Unexpected error. Error detail: %s" % str(e))
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
don't
Expose the “guts” to a client: full stack trace, environment variables’ values, etc. Too implementation-specific or “low-level” error messages
File "/home/taiga/.virtualenvs/taiga/lib/python3.4/site-packages/django/db/models/fields/related.py", line 572, in __get__
rel_obj = qs.get()
File "/home/taiga/.virtualenvs/taiga/lib/python3.4/site-packages/django/db/models/query.py", line 357, in get
self.model._meta.object_name)
taiga.projects.userstories.models.DoesNotExist: UserStory matching query does not exist.
do
Use domain/API-specific exceptions with direct, precise, implementation-agnostic error messages written in plain language.
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
do
Use domain/API-specific exceptions with direct, precise, implementation-agnostic error messages written in plain language.
...
...
except forms.ValidationError:
return HttpResponse('Invalid email format', status=status.HTTP_400_BAD_REQUEST)
except Client.DoesNotExist:
return HttpResponse('Client does not exists', status=status.HTTP_400_BAD_REQUEST)
except BadSignature:
return HttpResponse('Invalid token.', status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return HttpResponse('Sorry, something is broken. We will look into that.',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
do
Handle custom codes
HTTP/1.1 400 Bad Request
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"name": "Invalid username",
"message": "The username is not valid.",
"code": 40001
}
HTTP/1.1 400 Bad Request
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"name": "Invalid email",
"message": "The email is not valid.",
"code": 40002
}
do
Handle custom codes: twitter
HTTP/1.1 403 Forbidden
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"errors": [
{
"code": 64,
"message": "Your account is suspended and is not permitted to access this feature"
}
]
}
do
Handle custom codes: twitter
do
Handle custom codes: facebook
HTTP/1.1 403 Forbidden
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
Content-Type: application/json; charset=UTF-8
{
"error": {
"message": "Message describing the error",
"type": "OAuthException",
"code": 190,
"error_subcode": 460,
"error_user_title": "A title",
"error_user_msg": "A message"
"fbtrace_id": "EJplcsCHuLu"
}
}
do
Handle custom codes: facebook
do
Handle custom codes: Salesforce marketing cloud
do
Provide a Help URL
[shell]HTTP/1.1 400 Bad Request
{ "type": "error", "status": 400, "code": "bad_request", "context_info": { "errors": [ { "reason": "expiration_not_allowed", "name": "unshared_at", "message": "Expiration cannot be set on a shared link with 'collaborators' access", "location": "entity-body" } ] }, "help_url": "https://box-content.readme.io/reference#errors", "message": "Bad Request", "request_id": "13628284550db80756c518" }
do
Provide a Help URL: box
do
Provide a Help URL: twilio
Resources
http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#eafp-vs-lbyl
https://www.servage.net/blog/2013/04/08/rest-principles-explained/
http://www.slideshare.net/pydanny/python-worst-practices
https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
http://www.yiiframework.com/doc-2.0/guide-rest-error-handling.html
http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
http://scriptin.github.io/2015-02-08/restful-http-error-handling.html
http://caines.ca/blog/2013/04/21/3-terrible-anti-patterns-for-error-handling-in-rest-apis/
http://www.soa-probe.com/2013/02/layer-7-on-rest-api-error-handling.html
http://www.restapitutorial.com/httpstatuscodes.html
https://realpython.com/blog/python/the-most-diabolical-python-antipattern/
http://www.codingpedia.org/ama/resources-on-how-to-design-error-handling-in-a-rest-api/
https://www.box.com/blog/get-developer-hugs-with-rich-error-handling-in-your-api/
http://blog.cloud-elements.com/error-handling-restful-api-design-part-iii
https://developers.google.com/drive/v3/web/handle-errors
https://www.jeffknupp.com/blog/2013/02/06/write-cleaner-python-use-exceptions/
http://tutorials.jenkov.com/java-exception-handling/validation-throw-exception-or-return-false.html
http://www.wiredmonk.me/error-handling-and-logging-in-flask-restful.html
http://apigee.com/about/blog/technology/restful-api-design-what-about-errors
Social
Thanks!
Q & A
Error handling in RESTful API's
By Jhon Jairo Roa
Error handling in RESTful API's
Set of do's and don'ts when dealing with errors in RESTful API's
- 2,765