Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 # (c) 2005 Ian Bicking, Clark C. Evans and contributors
      4 # This module is part of the Python Paste Project and is released under
      5 # the MIT License: http://www.opensource.org/licenses/mit-license.php
      6 # Some of this code was funded by http://prometheusresearch.com
      7 """
      8 HTTP Exception Middleware
      9 
     10 This module processes Python exceptions that relate to HTTP exceptions
     11 by defining a set of exceptions, all subclasses of HTTPException, and a
     12 request handler (`middleware`) that catches these exceptions and turns
     13 them into proper responses.
     14 
     15 This module defines exceptions according to RFC 2068 [1]_ : codes with
     16 100-300 are not really errors; 400's are client errors, and 500's are
     17 server errors.  According to the WSGI specification [2]_ , the application
     18 can call ``start_response`` more then once only under two conditions:
     19 (a) the response has not yet been sent, or (b) if the second and
     20 subsequent invocations of ``start_response`` have a valid ``exc_info``
     21 argument obtained from ``sys.exc_info()``.  The WSGI specification then
     22 requires the server or gateway to handle the case where content has been
     23 sent and then an exception was encountered.
     24 
     25 Exceptions in the 5xx range and those raised after ``start_response``
     26 has been called are treated as serious errors and the ``exc_info`` is
     27 filled-in with information needed for a lower level module to generate a
     28 stack trace and log information.
     29 
     30 Exception
     31   HTTPException
     32     HTTPRedirection
     33       * 300 - HTTPMultipleChoices
     34       * 301 - HTTPMovedPermanently
     35       * 302 - HTTPFound
     36       * 303 - HTTPSeeOther
     37       * 304 - HTTPNotModified
     38       * 305 - HTTPUseProxy
     39       * 306 - Unused (not implemented, obviously)
     40       * 307 - HTTPTemporaryRedirect
     41     HTTPError
     42       HTTPClientError
     43         * 400 - HTTPBadRequest
     44         * 401 - HTTPUnauthorized
     45         * 402 - HTTPPaymentRequired
     46         * 403 - HTTPForbidden
     47         * 404 - HTTPNotFound
     48         * 405 - HTTPMethodNotAllowed
     49         * 406 - HTTPNotAcceptable
     50         * 407 - HTTPProxyAuthenticationRequired
     51         * 408 - HTTPRequestTimeout
     52         * 409 - HTTPConfict
     53         * 410 - HTTPGone
     54         * 411 - HTTPLengthRequired
     55         * 412 - HTTPPreconditionFailed
     56         * 413 - HTTPRequestEntityTooLarge
     57         * 414 - HTTPRequestURITooLong
     58         * 415 - HTTPUnsupportedMediaType
     59         * 416 - HTTPRequestRangeNotSatisfiable
     60         * 417 - HTTPExpectationFailed
     61         * 429 - HTTPTooManyRequests
     62       HTTPServerError
     63         * 500 - HTTPInternalServerError
     64         * 501 - HTTPNotImplemented
     65         * 502 - HTTPBadGateway
     66         * 503 - HTTPServiceUnavailable
     67         * 504 - HTTPGatewayTimeout
     68         * 505 - HTTPVersionNotSupported
     69 
     70 References:
     71 
     72 .. [1] http://www.python.org/peps/pep-0333.html#error-handling
     73 .. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
     74 
     75 """
     76 
     77 import six
     78 from paste.wsgilib import catch_errors_app
     79 from paste.response import has_header, header_value, replace_header
     80 from paste.request import resolve_relative_url
     81 from paste.util.quoting import strip_html, html_quote, no_quote, comment_quote
     82 
     83 SERVER_NAME = 'WSGI Server'
     84 TEMPLATE = """\
     85 <html>\r
     86   <head><title>%(title)s</title></head>\r
     87   <body>\r
     88     <h1>%(title)s</h1>\r
     89     <p>%(body)s</p>\r
     90     <hr noshade>\r
     91     <div align="right">%(server)s</div>\r
     92   </body>\r
     93 </html>\r
     94 """
     95 
     96 class HTTPException(Exception):
     97     """
     98     the HTTP exception base class
     99 
    100     This encapsulates an HTTP response that interrupts normal application
    101     flow; but one which is not necessarly an error condition. For
    102     example, codes in the 300's are exceptions in that they interrupt
    103     normal processing; however, they are not considered errors.
    104 
    105     This class is complicated by 4 factors:
    106 
    107       1. The content given to the exception may either be plain-text or
    108          as html-text.
    109 
    110       2. The template may want to have string-substitutions taken from
    111          the current ``environ`` or values from incoming headers. This
    112          is especially troublesome due to case sensitivity.
    113 
    114       3. The final output may either be text/plain or text/html
    115          mime-type as requested by the client application.
    116 
    117       4. Each exception has a default explanation, but those who
    118          raise exceptions may want to provide additional detail.
    119 
    120     Attributes:
    121 
    122        ``code``
    123            the HTTP status code for the exception
    124 
    125        ``title``
    126            remainder of the status line (stuff after the code)
    127 
    128        ``explanation``
    129            a plain-text explanation of the error message that is
    130            not subject to environment or header substitutions;
    131            it is accessible in the template via %(explanation)s
    132 
    133        ``detail``
    134            a plain-text message customization that is not subject
    135            to environment or header substitutions; accessible in
    136            the template via %(detail)s
    137 
    138        ``template``
    139            a content fragment (in HTML) used for environment and
    140            header substitution; the default template includes both
    141            the explanation and further detail provided in the
    142            message
    143 
    144        ``required_headers``
    145            a sequence of headers which are required for proper
    146            construction of the exception
    147 
    148     Parameters:
    149 
    150        ``detail``
    151          a plain-text override of the default ``detail``
    152 
    153        ``headers``
    154          a list of (k,v) header pairs
    155 
    156        ``comment``
    157          a plain-text additional information which is
    158          usually stripped/hidden for end-users
    159 
    160     To override the template (which is HTML content) or the plain-text
    161     explanation, one must subclass the given exception; or customize it
    162     after it has been created.  This particular breakdown of a message
    163     into explanation, detail and template allows both the creation of
    164     plain-text and html messages for various clients as well as
    165     error-free substitution of environment variables and headers.
    166     """
    167 
    168     code = None
    169     title = None
    170     explanation = ''
    171     detail = ''
    172     comment = ''
    173     template = "%(explanation)s\r\n<br/>%(detail)s\r\n<!-- %(comment)s -->"
    174     required_headers = ()
    175 
    176     def __init__(self, detail=None, headers=None, comment=None):
    177         assert self.code, "Do not directly instantiate abstract exceptions."
    178         assert isinstance(headers, (type(None), list)), (
    179             "headers must be None or a list: %r"
    180             % headers)
    181         assert isinstance(detail, (type(None), six.binary_type, six.text_type)), (
    182             "detail must be None or a string: %r" % detail)
    183         assert isinstance(comment, (type(None), six.binary_type, six.text_type)), (
    184             "comment must be None or a string: %r" % comment)
    185         self.headers = headers or tuple()
    186         for req in self.required_headers:
    187             assert headers and has_header(headers, req), (
    188                 "Exception %s must be passed the header %r "
    189                 "(got headers: %r)"
    190                 % (self.__class__.__name__, req, headers))
    191         if detail is not None:
    192             self.detail = detail
    193         if comment is not None:
    194             self.comment = comment
    195         Exception.__init__(self,"%s %s\n%s\n%s\n" % (
    196             self.code, self.title, self.explanation, self.detail))
    197 
    198     def make_body(self, environ, template, escfunc, comment_escfunc=None):
    199         comment_escfunc = comment_escfunc or escfunc
    200         args = {'explanation': escfunc(self.explanation),
    201                 'detail': escfunc(self.detail),
    202                 'comment': comment_escfunc(self.comment)}
    203         if HTTPException.template != self.template:
    204             for (k, v) in environ.items():
    205                 args[k] = escfunc(v)
    206             if self.headers:
    207                 for (k, v) in self.headers:
    208                     args[k.lower()] = escfunc(v)
    209         if six.PY2:
    210             for key, value in args.items():
    211                 if isinstance(value, six.text_type):
    212                     args[key] = value.encode('utf8', 'xmlcharrefreplace')
    213         return template % args
    214 
    215     def plain(self, environ):
    216         """ text/plain representation of the exception """
    217         body = self.make_body(environ, strip_html(self.template), no_quote, comment_quote)
    218         return ('%s %s\r\n%s\r\n' % (self.code, self.title, body))
    219 
    220     def html(self, environ):
    221         """ text/html representation of the exception """
    222         body = self.make_body(environ, self.template, html_quote, comment_quote)
    223         return TEMPLATE % {
    224                    'title': self.title,
    225                    'code': self.code,
    226                    'server': SERVER_NAME,
    227                    'body': body }
    228 
    229     def prepare_content(self, environ):
    230         if self.headers:
    231             headers = list(self.headers)
    232         else:
    233             headers = []
    234         if 'html' in environ.get('HTTP_ACCEPT','') or \
    235             '*/*' in environ.get('HTTP_ACCEPT',''):
    236             replace_header(headers, 'content-type', 'text/html')
    237             content = self.html(environ)
    238         else:
    239             replace_header(headers, 'content-type', 'text/plain')
    240             content = self.plain(environ)
    241         if isinstance(content, six.text_type):
    242             content = content.encode('utf8')
    243             cur_content_type = (
    244                 header_value(headers, 'content-type')
    245                 or 'text/html')
    246             replace_header(
    247                 headers, 'content-type',
    248                 cur_content_type + '; charset=utf8')
    249         return headers, content
    250 
    251     def response(self, environ):
    252         from paste.wsgiwrappers import WSGIResponse
    253         headers, content = self.prepare_content(environ)
    254         resp = WSGIResponse(code=self.code, content=content)
    255         resp.headers = resp.headers.fromlist(headers)
    256         return resp
    257 
    258     def wsgi_application(self, environ, start_response, exc_info=None):
    259         """
    260         This exception as a WSGI application
    261         """
    262         headers, content = self.prepare_content(environ)
    263         start_response('%s %s' % (self.code, self.title),
    264                        headers,
    265                        exc_info)
    266         return [content]
    267 
    268     __call__ = wsgi_application
    269 
    270     def __repr__(self):
    271         return '<%s %s; code=%s>' % (self.__class__.__name__,
    272                                      self.title, self.code)
    273 
    274 class HTTPError(HTTPException):
    275     """
    276     base class for status codes in the 400's and 500's
    277 
    278     This is an exception which indicates that an error has occurred,
    279     and that any work in progress should not be committed.  These are
    280     typically results in the 400's and 500's.
    281     """
    282 
    283 #
    284 # 3xx Redirection
    285 #
    286 #  This class of status code indicates that further action needs to be
    287 #  taken by the user agent in order to fulfill the request. The action
    288 #  required MAY be carried out by the user agent without interaction with
    289 #  the user if and only if the method used in the second request is GET or
    290 #  HEAD. A client SHOULD detect infinite redirection loops, since such
    291 #  loops generate network traffic for each redirection.
    292 #
    293 
    294 class HTTPRedirection(HTTPException):
    295     """
    296     base class for 300's status code (redirections)
    297 
    298     This is an abstract base class for 3xx redirection.  It indicates
    299     that further action needs to be taken by the user agent in order
    300     to fulfill the request.  It does not necessarly signal an error
    301     condition.
    302     """
    303 
    304 class _HTTPMove(HTTPRedirection):
    305     """
    306     redirections which require a Location field
    307 
    308     Since a 'Location' header is a required attribute of 301, 302, 303,
    309     305 and 307 (but not 304), this base class provides the mechanics to
    310     make this easy.  While this has the same parameters as HTTPException,
    311     if a location is not provided in the headers; it is assumed that the
    312     detail _is_ the location (this for backward compatibility, otherwise
    313     we'd add a new attribute).
    314     """
    315     required_headers = ('location',)
    316     explanation = 'The resource has been moved to'
    317     template = (
    318         '%(explanation)s <a href="%(location)s">%(location)s</a>;\r\n'
    319         'you should be redirected automatically.\r\n'
    320         '%(detail)s\r\n<!-- %(comment)s -->')
    321 
    322     def __init__(self, detail=None, headers=None, comment=None):
    323         assert isinstance(headers, (type(None), list))
    324         headers = headers or []
    325         location = header_value(headers,'location')
    326         if not location:
    327             location = detail
    328             detail = ''
    329             headers.append(('location', location))
    330         assert location, ("HTTPRedirection specified neither a "
    331                           "location in the headers nor did it "
    332                           "provide a detail argument.")
    333         HTTPRedirection.__init__(self, location, headers, comment)
    334         if detail is not None:
    335             self.detail = detail
    336 
    337     def relative_redirect(cls, dest_uri, environ, detail=None, headers=None, comment=None):
    338         """
    339         Create a redirect object with the dest_uri, which may be relative,
    340         considering it relative to the uri implied by the given environ.
    341         """
    342         location = resolve_relative_url(dest_uri, environ)
    343         headers = headers or []
    344         headers.append(('Location', location))
    345         return cls(detail=detail, headers=headers, comment=comment)
    346 
    347     relative_redirect = classmethod(relative_redirect)
    348 
    349     def location(self):
    350         for name, value in self.headers:
    351             if name.lower() == 'location':
    352                 return value
    353         else:
    354             raise KeyError("No location set for %s" % self)
    355 
    356 class HTTPMultipleChoices(_HTTPMove):
    357     code = 300
    358     title = 'Multiple Choices'
    359 
    360 class HTTPMovedPermanently(_HTTPMove):
    361     code = 301
    362     title = 'Moved Permanently'
    363 
    364 class HTTPFound(_HTTPMove):
    365     code = 302
    366     title = 'Found'
    367     explanation = 'The resource was found at'
    368 
    369 # This one is safe after a POST (the redirected location will be
    370 # retrieved with GET):
    371 class HTTPSeeOther(_HTTPMove):
    372     code = 303
    373     title = 'See Other'
    374 
    375 class HTTPNotModified(HTTPRedirection):
    376     # @@: but not always (HTTP section 14.18.1)...?
    377     # @@: Removed 'date' requirement, as its not required for an ETag
    378     # @@: FIXME: This should require either an ETag or a date header
    379     code = 304
    380     title = 'Not Modified'
    381     message = ''
    382     # @@: should include date header, optionally other headers
    383     # @@: should not return a content body
    384     def plain(self, environ):
    385         return ''
    386     def html(self, environ):
    387         """ text/html representation of the exception """
    388         return ''
    389 
    390 class HTTPUseProxy(_HTTPMove):
    391     # @@: OK, not a move, but looks a little like one
    392     code = 305
    393     title = 'Use Proxy'
    394     explanation = (
    395         'The resource must be accessed through a proxy '
    396         'located at')
    397 
    398 class HTTPTemporaryRedirect(_HTTPMove):
    399     code = 307
    400     title = 'Temporary Redirect'
    401 
    402 #
    403 # 4xx Client Error
    404 #
    405 #  The 4xx class of status code is intended for cases in which the client
    406 #  seems to have erred. Except when responding to a HEAD request, the
    407 #  server SHOULD include an entity containing an explanation of the error
    408 #  situation, and whether it is a temporary or permanent condition. These
    409 #  status codes are applicable to any request method. User agents SHOULD
    410 #  display any included entity to the user.
    411 #
    412 
    413 class HTTPClientError(HTTPError):
    414     """
    415     base class for the 400's, where the client is in-error
    416 
    417     This is an error condition in which the client is presumed to be
    418     in-error.  This is an expected problem, and thus is not considered
    419     a bug.  A server-side traceback is not warranted.  Unless specialized,
    420     this is a '400 Bad Request'
    421     """
    422     code = 400
    423     title = 'Bad Request'
    424     explanation = ('The server could not comply with the request since\r\n'
    425                    'it is either malformed or otherwise incorrect.\r\n')
    426 
    427 class HTTPBadRequest(HTTPClientError):
    428     pass
    429 
    430 class HTTPUnauthorized(HTTPClientError):
    431     code = 401
    432     title = 'Unauthorized'
    433     explanation = (
    434         'This server could not verify that you are authorized to\r\n'
    435         'access the document you requested.  Either you supplied the\r\n'
    436         'wrong credentials (e.g., bad password), or your browser\r\n'
    437         'does not understand how to supply the credentials required.\r\n')
    438 
    439 class HTTPPaymentRequired(HTTPClientError):
    440     code = 402
    441     title = 'Payment Required'
    442     explanation = ('Access was denied for financial reasons.')
    443 
    444 class HTTPForbidden(HTTPClientError):
    445     code = 403
    446     title = 'Forbidden'
    447     explanation = ('Access was denied to this resource.')
    448 
    449 class HTTPNotFound(HTTPClientError):
    450     code = 404
    451     title = 'Not Found'
    452     explanation = ('The resource could not be found.')
    453 
    454 class HTTPMethodNotAllowed(HTTPClientError):
    455     required_headers = ('allow',)
    456     code = 405
    457     title = 'Method Not Allowed'
    458     # override template since we need an environment variable
    459     template = ('The method %(REQUEST_METHOD)s is not allowed for '
    460                 'this resource.\r\n%(detail)s')
    461 
    462 class HTTPNotAcceptable(HTTPClientError):
    463     code = 406
    464     title = 'Not Acceptable'
    465     # override template since we need an environment variable
    466     template = ('The resource could not be generated that was '
    467                 'acceptable to your browser (content\r\nof type '
    468                 '%(HTTP_ACCEPT)s).\r\n%(detail)s')
    469 
    470 class HTTPProxyAuthenticationRequired(HTTPClientError):
    471     code = 407
    472     title = 'Proxy Authentication Required'
    473     explanation = ('Authentication /w a local proxy is needed.')
    474 
    475 class HTTPRequestTimeout(HTTPClientError):
    476     code = 408
    477     title = 'Request Timeout'
    478     explanation = ('The server has waited too long for the request to '
    479                    'be sent by the client.')
    480 
    481 class HTTPConflict(HTTPClientError):
    482     code = 409
    483     title = 'Conflict'
    484     explanation = ('There was a conflict when trying to complete '
    485                    'your request.')
    486 
    487 class HTTPGone(HTTPClientError):
    488     code = 410
    489     title = 'Gone'
    490     explanation = ('This resource is no longer available.  No forwarding '
    491                    'address is given.')
    492 
    493 class HTTPLengthRequired(HTTPClientError):
    494     code = 411
    495     title = 'Length Required'
    496     explanation = ('Content-Length header required.')
    497 
    498 class HTTPPreconditionFailed(HTTPClientError):
    499     code = 412
    500     title = 'Precondition Failed'
    501     explanation = ('Request precondition failed.')
    502 
    503 class HTTPRequestEntityTooLarge(HTTPClientError):
    504     code = 413
    505     title = 'Request Entity Too Large'
    506     explanation = ('The body of your request was too large for this server.')
    507 
    508 class HTTPRequestURITooLong(HTTPClientError):
    509     code = 414
    510     title = 'Request-URI Too Long'
    511     explanation = ('The request URI was too long for this server.')
    512 
    513 class HTTPUnsupportedMediaType(HTTPClientError):
    514     code = 415
    515     title = 'Unsupported Media Type'
    516     # override template since we need an environment variable
    517     template = ('The request media type %(CONTENT_TYPE)s is not '
    518                 'supported by this server.\r\n%(detail)s')
    519 
    520 class HTTPRequestRangeNotSatisfiable(HTTPClientError):
    521     code = 416
    522     title = 'Request Range Not Satisfiable'
    523     explanation = ('The Range requested is not available.')
    524 
    525 class HTTPExpectationFailed(HTTPClientError):
    526     code = 417
    527     title = 'Expectation Failed'
    528     explanation = ('Expectation failed.')
    529 
    530 class HTTPTooManyRequests(HTTPClientError):
    531     code = 429
    532     title = 'Too Many Requests'
    533     explanation = ('The client has sent too many requests to the server.')
    534 
    535 #
    536 # 5xx Server Error
    537 #
    538 #  Response status codes beginning with the digit "5" indicate cases in
    539 #  which the server is aware that it has erred or is incapable of
    540 #  performing the request. Except when responding to a HEAD request, the
    541 #  server SHOULD include an entity containing an explanation of the error
    542 #  situation, and whether it is a temporary or permanent condition. User
    543 #  agents SHOULD display any included entity to the user. These response
    544 #  codes are applicable to any request method.
    545 #
    546 
    547 class HTTPServerError(HTTPError):
    548     """
    549     base class for the 500's, where the server is in-error
    550 
    551     This is an error condition in which the server is presumed to be
    552     in-error.  This is usually unexpected, and thus requires a traceback;
    553     ideally, opening a support ticket for the customer. Unless specialized,
    554     this is a '500 Internal Server Error'
    555     """
    556     code = 500
    557     title = 'Internal Server Error'
    558     explanation = (
    559       'The server has either erred or is incapable of performing\r\n'
    560       'the requested operation.\r\n')
    561 
    562 class HTTPInternalServerError(HTTPServerError):
    563     pass
    564 
    565 class HTTPNotImplemented(HTTPServerError):
    566     code = 501
    567     title = 'Not Implemented'
    568     # override template since we need an environment variable
    569     template = ('The request method %(REQUEST_METHOD)s is not implemented '
    570                 'for this server.\r\n%(detail)s')
    571 
    572 class HTTPBadGateway(HTTPServerError):
    573     code = 502
    574     title = 'Bad Gateway'
    575     explanation = ('Bad gateway.')
    576 
    577 class HTTPServiceUnavailable(HTTPServerError):
    578     code = 503
    579     title = 'Service Unavailable'
    580     explanation = ('The server is currently unavailable. '
    581                    'Please try again at a later time.')
    582 
    583 class HTTPGatewayTimeout(HTTPServerError):
    584     code = 504
    585     title = 'Gateway Timeout'
    586     explanation = ('The gateway has timed out.')
    587 
    588 class HTTPVersionNotSupported(HTTPServerError):
    589     code = 505
    590     title = 'HTTP Version Not Supported'
    591     explanation = ('The HTTP version is not supported.')
    592 
    593 # abstract HTTP related exceptions
    594 __all__ = ['HTTPException', 'HTTPRedirection', 'HTTPError' ]
    595 
    596 _exceptions = {}
    597 for name, value in six.iteritems(dict(globals())):
    598     if (isinstance(value, (type, six.class_types)) and
    599         issubclass(value, HTTPException) and
    600         value.code):
    601         _exceptions[value.code] = value
    602         __all__.append(name)
    603 
    604 def get_exception(code):
    605     return _exceptions[code]
    606 
    607 ############################################################
    608 ## Middleware implementation:
    609 ############################################################
    610 
    611 class HTTPExceptionHandler(object):
    612     """
    613     catches exceptions and turns them into proper HTTP responses
    614 
    615     This middleware catches any exceptions (which are subclasses of
    616     ``HTTPException``) and turns them into proper HTTP responses.
    617     Note if the headers have already been sent, the stack trace is
    618     always maintained as this indicates a programming error.
    619 
    620     Note that you must raise the exception before returning the
    621     app_iter, and you cannot use this with generator apps that don't
    622     raise an exception until after their app_iter is iterated over.
    623     """
    624 
    625     def __init__(self, application, warning_level=None):
    626         assert not warning_level or ( warning_level > 99 and
    627                                       warning_level < 600)
    628         if warning_level is not None:
    629             import warnings
    630             warnings.warn('The warning_level parameter is not used or supported',
    631                           DeprecationWarning, 2)
    632         self.warning_level = warning_level or 500
    633         self.application = application
    634 
    635     def __call__(self, environ, start_response):
    636         environ['paste.httpexceptions'] = self
    637         environ.setdefault('paste.expected_exceptions',
    638                            []).append(HTTPException)
    639         try:
    640             return self.application(environ, start_response)
    641         except HTTPException as exc:
    642             return exc(environ, start_response)
    643 
    644 def middleware(*args, **kw):
    645     import warnings
    646     # deprecated 13 dec 2005
    647     warnings.warn('httpexceptions.middleware is deprecated; use '
    648                   'make_middleware or HTTPExceptionHandler instead',
    649                   DeprecationWarning, 2)
    650     return make_middleware(*args, **kw)
    651 
    652 def make_middleware(app, global_conf=None, warning_level=None):
    653     """
    654     ``httpexceptions`` middleware; this catches any
    655     ``paste.httpexceptions.HTTPException`` exceptions (exceptions like
    656     ``HTTPNotFound``, ``HTTPMovedPermanently``, etc) and turns them
    657     into proper HTTP responses.
    658 
    659     ``warning_level`` can be an integer corresponding to an HTTP code.
    660     Any code over that value will be passed 'up' the chain, potentially
    661     reported on by another piece of middleware.
    662     """
    663     if warning_level:
    664         warning_level = int(warning_level)
    665     return HTTPExceptionHandler(app, warning_level=warning_level)
    666 
    667 __all__.extend(['HTTPExceptionHandler', 'get_exception'])
    668