RFC-7807 implementation for Tornado¶
This library provides a version of tornado.web.RequestHandler.send_error
that speaks application/problem+json
instead of HTML. The easiest
way to use this library is to inherit from problemdetails.ErrorWriter
and raise problemdetails.Problem
exceptions instead of HTTPError
.
class MyHandler(problemdetails.ErrorWriter, web.RequestHandler):
def get(self):
if not self.do_something_hard():
raise problemdetails.Problem(status_code=500,
title='Failed to do_something_hard')
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{
"status": 500,
"title": "Failed to do_something_hard",
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1"
}
You can easily construct more substantial response documents by passing
additional keyword parameters to the problemdetails.Problem
initializer. They become top-level properties in the response document.
You can also call send_error
directly and produce a response docuemnt.
The following snippet produces the same output as the previous snippet.
class MyHandler(problemdetails.ErrorWriter, web.RequestHandler):
def get(self):
try:
self.do_something_hard()
except SomeException as error:
self.send_error(500, title="Failed to do_something_hard")
The interface of tornado.web.RequestHandler.send_error
is less expressive
since keyword parameters may be swallowed by intervening code. The only
parameters that are recognized are: instance
, title
, and type
.
Use the exception-based interface for more substantial documents.
Examples¶
You can run any of the examples in this section by preparing a Python environment:
$ python3 -mvenv env
$ env/bin/pip install -qe .
$ env/bin/python examples/httpbin.py
D - asyncio: Using selector: KqueueSelector
I - root: listening on http://127.0.0.1:8000
I - root: GET http://127.0.0.1:8000/?status=419
The examples run the application in debug mode so changing the source code results in the application reloading immediately. Feel free to play with the examples.
httpbin.org¶
This example sends a request to httpbin.org that generates a HTTP failure.
The failure is caught as a tornado.web.HTTPError
instance and
translated into a problemdetails.Problem
instance. You can run
this example with python examples/httpbin.py
and send requests to
http://localhost:8000/?status=500
.
@gen.coroutine
def get(self):
logger = logging.getLogger('HttpBinHandler')
url = 'http://httpbin.org/status/{0}'.format(
self.get_query_argument('status', '500'))
logger.info('retrieving %s', url)
client = httpclient.AsyncHTTPClient()
try:
response = yield client.fetch(url)
self.add_header('Content-Type', 'application/json')
self.write(response.body)
except httpclient.HTTPError as error:
raise problemdetails.Problem(
status_code=error.code,
httpbin_headers=dict(error.response.headers.items()))
RFC-7807 examples¶
This example includes two endpoints that return the examples from RFC 7807. The first example returns the 403 credit error from section 3. This example exemplifies raising an exception where the failure is detected and finishing the request processing by not catching the exception. The raised exception is fairly complex but constructed from literals.
class AccountHandler(problemdetails.ErrorWriter, web.RequestHandler):
def post(self, account):
price = float(self.get_query_argument('price'))
self.verify_funds(account, price)
self.buy_thing()
def verify_funds(self, account, price):
balance = self.get_balance(account)
if price > balance:
raise problemdetails.Problem(
status_code=403,
title='You do not have enough credit.',
detail=(f'Your current balance is {balance}, but that '
f'costs {price}'),
instance=self.reverse_url('account-handler', account),
balance=balance,
accounts=self.lookup_accounts(account),
type='https://example.com/probs/out-of-credit',
)
def get_balance(self, account):
return 30.0 # TODO look up balance
def lookup_accounts(self, primary_account):
related = [12345, 67890] # TODO lookup related accounts
return [self.reverse_url('account-handler', aid) for aid in related]
def buy_thing(self):
pass
The second example returns the more complex 400 invalid parameter error from section 3. The errors are accumulated into a list and raised from the top-level after the parameters are extracted. This is a different approach where all of the error handling is explicit and at the top-level.
class ValidationError(problemdetails.ErrorWriter, web.RequestHandler):
def initialize(self):
super(ValidationError, self).initialize()
self.errors = []
def get(self):
age = self._extract_age()
color = self._extract_color()
if self.errors:
raise problemdetails.Problem(**{
'status_code': 400,
'type': 'https://example.net/validation-error',
'invalid-params': self.errors,
})
self.write({'age': age, 'color': color})
def _record_error(self, field, error):
self.errors.append({'name': field, 'reason': error})
def _extract_age(self):
try:
age = self.get_query_argument('age')
except web.MissingArgumentError:
self._record_error('age', 'is required')
else:
try:
age = int(age)
if age > 0:
return age
self._record_error('age', 'must be a positive integer')
except ValueError:
self._record_error('age', 'must be a integer')
return None
def _extract_color(self):
try:
color = self.get_query_argument('color')
except web.MissingArgumentError:
self._record_error('color', 'is required')
else:
if color in ('blue', 'green', 'red'):
return color
self._record_error('color', "must be 'green', 'red' or 'blue'")
return None
Reference¶
-
class
problemdetails.
ErrorWriter
(application: tornado.web.Application, request: tornado.httputil.HTTPServerRequest, **kwargs)[source]¶ Render application/problem+json in
write_error
Include this class in the base class list to return errors as application/problem+json documents instead of HTML.
-
PROBLEM_DETAILS_MIME_TYPE
¶ This class-level attribute controls the content type used by write_error. Due to the way that write_error is called, it is necessary to configure this. Setting the content type header before calling
send_error()
DOES NOT work as expected.If you need to change the response content type, change this attribute at the class-level.
-
write_error
(status_code, **kwargs)[source]¶ Render application/problem+json documents instead of HTML.
- Parameters
status_code (int) – HTTP status code that we returned
detail (str) – optional detail field to include in the error document. This field is omitted by default.
instance (str) – optional instance to include in the error document. This field is omitted by default.
title (str) – optional title to include in the error document. THis field is omitted by default.
type (str) – optional type field to include in the error document. This field defaults to a link to the official HTTP specification of status_code if omitted and status_code is a standard code.
See RFC 7807#section-3.1 for a description of each optional field.
-
-
class
problemdetails.
Problem
(status_code, log_message=None, *args, **kwargs)[source]¶ An exception that will be translated into a json document.
- Parameters
status_code (int) – HTTP status code to return
log_message (str) – optional log message that is passed to the
tornado.web.HTTPError
initializerargs – parameters that are passed to log_message in the
HTTPError
initializer.reason (str) – optional reason phrase to use in the HTTP response line. This value is NOT included in the response document.
kwargs – additional keyword parameters are included in the response document
problemdetails.ErrorWriter.write_error()
recognizes this exception type and rendersdocument
as the problem+json result. The status property is set to status_code and the type property will be set bywrite_error
unless it is explicitly set.
-
problemdetails.
type_link_map
¶ Mapping of HTTP status code to type link.
This table maps HTTP status codes to the IANA registered specification for the code. You can add additional links or replace ones that are here as you see fit. The error writer uses this table to generate the default
type
link in responses.