Exceptions on the web service

Exceptions on the LAZR web service are handled like other exceptions occurring during zope publication: a view is looked-up and used to return the response to the client.

Setup

>>> from zope.component import getSiteManager, getUtility
>>> from zope.interface import implementer
>>> from lazr.restful.interfaces import IWebServiceConfiguration
>>> sm = getSiteManager()
>>> @implementer(IWebServiceConfiguration)
... class SimpleWebServiceConfiguration:
...     show_tracebacks = False
...     active_versions = ['trunk']
...     last_version_with_mutator_named_operations = None
>>> webservice_configuration = SimpleWebServiceConfiguration()
>>> sm.registerUtility(webservice_configuration)
>>> from lazr.restful.interfaces import IWebServiceVersion
>>> class ITestServiceRequestTrunk(IWebServiceVersion):
...     pass
>>> sm.registerUtility(
...     ITestServiceRequestTrunk, IWebServiceVersion, name='trunk')

WebServiceExceptionView

WebServiceExcpetionView is a generic view class that can handle exceptions during web service requests.

>>> from lazr.restful.error import WebServiceExceptionView
>>> def render_error_view(error, request):
...     """Create a WebServiceExceptionView to render the exception.
...
...     The exception is raised, because exception view can be expected
...     to be called from within an exception handler.
...     """
...     try:
...         raise error
...     except Exception as error:
...         return WebServiceExceptionView(error, request)()

That view returns the exception message as content, and sets the result code to the one specified using the webservice_error() directive.

>>> from lazr.restful.declarations import webservice_error
>>> class InvalidInput(Exception):
...     """Client provided invalid input."""
...     webservice_error(400)

Depending on the show_tracebacks setting, it may also print a traceback of the exception. Tracebacks are only shown for errors that have a 5xx http error code.

>>> from textwrap import dedent
>>> from lazr.restful.testing.webservice import FakeRequest
>>> webservice_configuration.show_tracebacks
False

When tracebacks are not shown, the view simply returns the exception message and sets the status code to the one related to the exception.

>>> request = FakeRequest()
>>> render_error_view(
...     InvalidInput("foo@bar isn't a valid email address"), request)
"foo@bar isn't a valid email address"
>>> request.response.headers['Content-Type']
'text/plain'
>>> request.response.status
400

When the request contains an OOPSID, it will be set in the X-Lazr-OopsId header:

>>> print(request.response.headers.get('X-Lazr-OopsId'))
None
>>> request = FakeRequest()
>>> request.oopsid = 'OOPS-001'
>>> ignored = render_error_view(InvalidInput('bad email'), request)
>>> print(request.response.headers['X-Lazr-OopsId'])
OOPS-001

Even if show_tracebacks is set to true, non-5xx error codes will not produce a traceback.

>>> webservice_configuration.show_tracebacks = True
>>> print(render_error_view(InvalidInput('bad email'), request))
bad email

Internal server errors

Exceptions that are server-side errors are handled a little differently.

>>> class ServerError(Exception):
...     """Something went wrong on the server side."""
...     webservice_error(500)

If show_tracebacks is True, the user is going to see a full traceback anyway, so there’s no point in hiding the exception message. When tracebacks are shown, the view puts a traceback dump in the response.

>>> print(render_error_view(ServerError('DB crash'), request))
DB crash

Traceback (most recent call last):
 ...
ServerError: DB crash

If show_tracebacks is False, on an internal server error they client will see the exception class name instead of a message.

>>> webservice_configuration.show_tracebacks = False
>>> print(render_error_view(ServerError('DB crash'), request))
ServerError

Default exceptions

Standard exceptions have a view registered for them by default.

>>> from zope.configuration import xmlconfig
>>> zcmlcontext = xmlconfig.string("""
... <configure xmlns="http://namespaces.zope.org/zope">
...   <include package="lazr.restful" file="basic-site.zcml"/>
... </configure>
... """)
>>> from zope.component import getMultiAdapter
>>> def render_using_default_view(error):
...     """Render an exception using its default 'index.html' view.
...     :return: response, result tuple. (The response object and
...         the content).
...     """
...     try:
...         raise error
...     except Exception as error:
...         request = FakeRequest()
...         view = getMultiAdapter((error, request), name="index.html")
...         result = view()
...         return request.response, result

NotFound exceptions have a 404 status code.

>>> from zope.publisher.interfaces import NotFound
>>> response, result = render_using_default_view(
...     NotFound(object(), 'name'))
>>> response.status
404

Unauthorized exceptions have a 401 status code.

>>> from zope.security.interfaces import Unauthorized
>>> response, result = render_using_default_view(Unauthorized())
>>> response.status
401

Other exceptions have the 500 status code.

>>> response, result = render_using_default_view(Exception())
>>> response.status
500