| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
- # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
- # (c) 2005 Ian Bicking, Clark C. Evans and contributors
- # This module is part of the Python Paste Project and is released under
- # the MIT License: http://www.opensource.org/licenses/mit-license.php
- # Some of this code was funded by http://prometheusresearch.com
- """
- HTTP Exception Middleware
- This module processes Python exceptions that relate to HTTP exceptions
- by defining a set of exceptions, all subclasses of HTTPException, and a
- request handler (`middleware`) that catches these exceptions and turns
- them into proper responses.
- This module defines exceptions according to RFC 2068 [1]_ : codes with
- 100-300 are not really errors; 400's are client errors, and 500's are
- server errors. According to the WSGI specification [2]_ , the application
- can call ``start_response`` more then once only under two conditions:
- (a) the response has not yet been sent, or (b) if the second and
- subsequent invocations of ``start_response`` have a valid ``exc_info``
- argument obtained from ``sys.exc_info()``. The WSGI specification then
- requires the server or gateway to handle the case where content has been
- sent and then an exception was encountered.
- Exceptions in the 5xx range and those raised after ``start_response``
- has been called are treated as serious errors and the ``exc_info`` is
- filled-in with information needed for a lower level module to generate a
- stack trace and log information.
- Exception
- HTTPException
- HTTPRedirection
- * 300 - HTTPMultipleChoices
- * 301 - HTTPMovedPermanently
- * 302 - HTTPFound
- * 303 - HTTPSeeOther
- * 304 - HTTPNotModified
- * 305 - HTTPUseProxy
- * 306 - Unused (not implemented, obviously)
- * 307 - HTTPTemporaryRedirect
- HTTPError
- HTTPClientError
- * 400 - HTTPBadRequest
- * 401 - HTTPUnauthorized
- * 402 - HTTPPaymentRequired
- * 403 - HTTPForbidden
- * 404 - HTTPNotFound
- * 405 - HTTPMethodNotAllowed
- * 406 - HTTPNotAcceptable
- * 407 - HTTPProxyAuthenticationRequired
- * 408 - HTTPRequestTimeout
- * 409 - HTTPConfict
- * 410 - HTTPGone
- * 411 - HTTPLengthRequired
- * 412 - HTTPPreconditionFailed
- * 413 - HTTPRequestEntityTooLarge
- * 414 - HTTPRequestURITooLong
- * 415 - HTTPUnsupportedMediaType
- * 416 - HTTPRequestRangeNotSatisfiable
- * 417 - HTTPExpectationFailed
- HTTPServerError
- * 500 - HTTPInternalServerError
- * 501 - HTTPNotImplemented
- * 502 - HTTPBadGateway
- * 503 - HTTPServiceUnavailable
- * 504 - HTTPGatewayTimeout
- * 505 - HTTPVersionNotSupported
- References:
- .. [1] http://www.python.org/peps/pep-0333.html#error-handling
- .. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
- """
- import six
- from paste.wsgilib import catch_errors_app
- from paste.response import has_header, header_value, replace_header
- from paste.request import resolve_relative_url
- from paste.util.quoting import strip_html, html_quote, no_quote, comment_quote
- SERVER_NAME = 'WSGI Server'
- TEMPLATE = """\
- <html>\r
- <head><title>%(title)s</title></head>\r
- <body>\r
- <h1>%(title)s</h1>\r
- <p>%(body)s</p>\r
- <hr noshade>\r
- <div align="right">%(server)s</div>\r
- </body>\r
- </html>\r
- """
- class HTTPException(Exception):
- """
- the HTTP exception base class
- This encapsulates an HTTP response that interrupts normal application
- flow; but one which is not necessarly an error condition. For
- example, codes in the 300's are exceptions in that they interrupt
- normal processing; however, they are not considered errors.
- This class is complicated by 4 factors:
- 1. The content given to the exception may either be plain-text or
- as html-text.
- 2. The template may want to have string-substitutions taken from
- the current ``environ`` or values from incoming headers. This
- is especially troublesome due to case sensitivity.
- 3. The final output may either be text/plain or text/html
- mime-type as requested by the client application.
- 4. Each exception has a default explanation, but those who
- raise exceptions may want to provide additional detail.
- Attributes:
- ``code``
- the HTTP status code for the exception
- ``title``
- remainder of the status line (stuff after the code)
- ``explanation``
- a plain-text explanation of the error message that is
- not subject to environment or header substitutions;
- it is accessible in the template via %(explanation)s
- ``detail``
- a plain-text message customization that is not subject
- to environment or header substitutions; accessible in
- the template via %(detail)s
- ``template``
- a content fragment (in HTML) used for environment and
- header substitution; the default template includes both
- the explanation and further detail provided in the
- message
- ``required_headers``
- a sequence of headers which are required for proper
- construction of the exception
- Parameters:
- ``detail``
- a plain-text override of the default ``detail``
- ``headers``
- a list of (k,v) header pairs
- ``comment``
- a plain-text additional information which is
- usually stripped/hidden for end-users
- To override the template (which is HTML content) or the plain-text
- explanation, one must subclass the given exception; or customize it
- after it has been created. This particular breakdown of a message
- into explanation, detail and template allows both the creation of
- plain-text and html messages for various clients as well as
- error-free substitution of environment variables and headers.
- """
- code = None
- title = None
- explanation = ''
- detail = ''
- comment = ''
- template = "%(explanation)s\r\n<br/>%(detail)s\r\n<!-- %(comment)s -->"
- required_headers = ()
- def __init__(self, detail=None, headers=None, comment=None):
- assert self.code, "Do not directly instantiate abstract exceptions."
- assert isinstance(headers, (type(None), list)), (
- "headers must be None or a list: %r"
- % headers)
- assert isinstance(detail, (type(None), six.binary_type, six.text_type)), (
- "detail must be None or a string: %r" % detail)
- assert isinstance(comment, (type(None), six.binary_type, six.text_type)), (
- "comment must be None or a string: %r" % comment)
- self.headers = headers or tuple()
- for req in self.required_headers:
- assert headers and has_header(headers, req), (
- "Exception %s must be passed the header %r "
- "(got headers: %r)"
- % (self.__class__.__name__, req, headers))
- if detail is not None:
- self.detail = detail
- if comment is not None:
- self.comment = comment
- Exception.__init__(self,"%s %s\n%s\n%s\n" % (
- self.code, self.title, self.explanation, self.detail))
- def make_body(self, environ, template, escfunc, comment_escfunc=None):
- comment_escfunc = comment_escfunc or escfunc
- args = {'explanation': escfunc(self.explanation),
- 'detail': escfunc(self.detail),
- 'comment': comment_escfunc(self.comment)}
- if HTTPException.template != self.template:
- for (k, v) in environ.items():
- args[k] = escfunc(v)
- if self.headers:
- for (k, v) in self.headers:
- args[k.lower()] = escfunc(v)
- if six.PY2:
- for key, value in args.items():
- if isinstance(value, six.text_type):
- args[key] = value.encode('utf8', 'xmlcharrefreplace')
- return template % args
- def plain(self, environ):
- """ text/plain representation of the exception """
- body = self.make_body(environ, strip_html(self.template), no_quote, comment_quote)
- return ('%s %s\r\n%s\r\n' % (self.code, self.title, body))
- def html(self, environ):
- """ text/html representation of the exception """
- body = self.make_body(environ, self.template, html_quote, comment_quote)
- return TEMPLATE % {
- 'title': self.title,
- 'code': self.code,
- 'server': SERVER_NAME,
- 'body': body }
- def prepare_content(self, environ):
- if self.headers:
- headers = list(self.headers)
- else:
- headers = []
- if 'html' in environ.get('HTTP_ACCEPT','') or \
- '*/*' in environ.get('HTTP_ACCEPT',''):
- replace_header(headers, 'content-type', 'text/html')
- content = self.html(environ)
- else:
- replace_header(headers, 'content-type', 'text/plain')
- content = self.plain(environ)
- if isinstance(content, six.text_type):
- content = content.encode('utf8')
- cur_content_type = (
- header_value(headers, 'content-type')
- or 'text/html')
- replace_header(
- headers, 'content-type',
- cur_content_type + '; charset=utf8')
- return headers, content
- def response(self, environ):
- from paste.wsgiwrappers import WSGIResponse
- headers, content = self.prepare_content(environ)
- resp = WSGIResponse(code=self.code, content=content)
- resp.headers = resp.headers.fromlist(headers)
- return resp
- def wsgi_application(self, environ, start_response, exc_info=None):
- """
- This exception as a WSGI application
- """
- headers, content = self.prepare_content(environ)
- start_response('%s %s' % (self.code, self.title),
- headers,
- exc_info)
- return [content]
- __call__ = wsgi_application
- def __repr__(self):
- return '<%s %s; code=%s>' % (self.__class__.__name__,
- self.title, self.code)
- class HTTPError(HTTPException):
- """
- base class for status codes in the 400's and 500's
- This is an exception which indicates that an error has occurred,
- and that any work in progress should not be committed. These are
- typically results in the 400's and 500's.
- """
- #
- # 3xx Redirection
- #
- # This class of status code indicates that further action needs to be
- # taken by the user agent in order to fulfill the request. The action
- # required MAY be carried out by the user agent without interaction with
- # the user if and only if the method used in the second request is GET or
- # HEAD. A client SHOULD detect infinite redirection loops, since such
- # loops generate network traffic for each redirection.
- #
- class HTTPRedirection(HTTPException):
- """
- base class for 300's status code (redirections)
- This is an abstract base class for 3xx redirection. It indicates
- that further action needs to be taken by the user agent in order
- to fulfill the request. It does not necessarly signal an error
- condition.
- """
- class _HTTPMove(HTTPRedirection):
- """
- redirections which require a Location field
- Since a 'Location' header is a required attribute of 301, 302, 303,
- 305 and 307 (but not 304), this base class provides the mechanics to
- make this easy. While this has the same parameters as HTTPException,
- if a location is not provided in the headers; it is assumed that the
- detail _is_ the location (this for backward compatibility, otherwise
- we'd add a new attribute).
- """
- required_headers = ('location',)
- explanation = 'The resource has been moved to'
- template = (
- '%(explanation)s <a href="%(location)s">%(location)s</a>;\r\n'
- 'you should be redirected automatically.\r\n'
- '%(detail)s\r\n<!-- %(comment)s -->')
- def __init__(self, detail=None, headers=None, comment=None):
- assert isinstance(headers, (type(None), list))
- headers = headers or []
- location = header_value(headers,'location')
- if not location:
- location = detail
- detail = ''
- headers.append(('location', location))
- assert location, ("HTTPRedirection specified neither a "
- "location in the headers nor did it "
- "provide a detail argument.")
- HTTPRedirection.__init__(self, location, headers, comment)
- if detail is not None:
- self.detail = detail
- def relative_redirect(cls, dest_uri, environ, detail=None, headers=None, comment=None):
- """
- Create a redirect object with the dest_uri, which may be relative,
- considering it relative to the uri implied by the given environ.
- """
- location = resolve_relative_url(dest_uri, environ)
- headers = headers or []
- headers.append(('Location', location))
- return cls(detail=detail, headers=headers, comment=comment)
- relative_redirect = classmethod(relative_redirect)
- def location(self):
- for name, value in self.headers:
- if name.lower() == 'location':
- return value
- else:
- raise KeyError("No location set for %s" % self)
- class HTTPMultipleChoices(_HTTPMove):
- code = 300
- title = 'Multiple Choices'
- class HTTPMovedPermanently(_HTTPMove):
- code = 301
- title = 'Moved Permanently'
- class HTTPFound(_HTTPMove):
- code = 302
- title = 'Found'
- explanation = 'The resource was found at'
- # This one is safe after a POST (the redirected location will be
- # retrieved with GET):
- class HTTPSeeOther(_HTTPMove):
- code = 303
- title = 'See Other'
- class HTTPNotModified(HTTPRedirection):
- # @@: but not always (HTTP section 14.18.1)...?
- # @@: Removed 'date' requirement, as its not required for an ETag
- # @@: FIXME: This should require either an ETag or a date header
- code = 304
- title = 'Not Modified'
- message = ''
- # @@: should include date header, optionally other headers
- # @@: should not return a content body
- def plain(self, environ):
- return ''
- def html(self, environ):
- """ text/html representation of the exception """
- return ''
- class HTTPUseProxy(_HTTPMove):
- # @@: OK, not a move, but looks a little like one
- code = 305
- title = 'Use Proxy'
- explanation = (
- 'The resource must be accessed through a proxy '
- 'located at')
- class HTTPTemporaryRedirect(_HTTPMove):
- code = 307
- title = 'Temporary Redirect'
- #
- # 4xx Client Error
- #
- # The 4xx class of status code is intended for cases in which the client
- # seems to have erred. Except when responding to a HEAD request, the
- # server SHOULD include an entity containing an explanation of the error
- # situation, and whether it is a temporary or permanent condition. These
- # status codes are applicable to any request method. User agents SHOULD
- # display any included entity to the user.
- #
- class HTTPClientError(HTTPError):
- """
- base class for the 400's, where the client is in-error
- This is an error condition in which the client is presumed to be
- in-error. This is an expected problem, and thus is not considered
- a bug. A server-side traceback is not warranted. Unless specialized,
- this is a '400 Bad Request'
- """
- code = 400
- title = 'Bad Request'
- explanation = ('The server could not comply with the request since\r\n'
- 'it is either malformed or otherwise incorrect.\r\n')
- class HTTPBadRequest(HTTPClientError):
- pass
- class HTTPUnauthorized(HTTPClientError):
- code = 401
- title = 'Unauthorized'
- explanation = (
- 'This server could not verify that you are authorized to\r\n'
- 'access the document you requested. Either you supplied the\r\n'
- 'wrong credentials (e.g., bad password), or your browser\r\n'
- 'does not understand how to supply the credentials required.\r\n')
- class HTTPPaymentRequired(HTTPClientError):
- code = 402
- title = 'Payment Required'
- explanation = ('Access was denied for financial reasons.')
- class HTTPForbidden(HTTPClientError):
- code = 403
- title = 'Forbidden'
- explanation = ('Access was denied to this resource.')
- class HTTPNotFound(HTTPClientError):
- code = 404
- title = 'Not Found'
- explanation = ('The resource could not be found.')
- class HTTPMethodNotAllowed(HTTPClientError):
- required_headers = ('allow',)
- code = 405
- title = 'Method Not Allowed'
- # override template since we need an environment variable
- template = ('The method %(REQUEST_METHOD)s is not allowed for '
- 'this resource.\r\n%(detail)s')
- class HTTPNotAcceptable(HTTPClientError):
- code = 406
- title = 'Not Acceptable'
- # override template since we need an environment variable
- template = ('The resource could not be generated that was '
- 'acceptable to your browser (content\r\nof type '
- '%(HTTP_ACCEPT)s).\r\n%(detail)s')
- class HTTPProxyAuthenticationRequired(HTTPClientError):
- code = 407
- title = 'Proxy Authentication Required'
- explanation = ('Authentication /w a local proxy is needed.')
- class HTTPRequestTimeout(HTTPClientError):
- code = 408
- title = 'Request Timeout'
- explanation = ('The server has waited too long for the request to '
- 'be sent by the client.')
- class HTTPConflict(HTTPClientError):
- code = 409
- title = 'Conflict'
- explanation = ('There was a conflict when trying to complete '
- 'your request.')
- class HTTPGone(HTTPClientError):
- code = 410
- title = 'Gone'
- explanation = ('This resource is no longer available. No forwarding '
- 'address is given.')
- class HTTPLengthRequired(HTTPClientError):
- code = 411
- title = 'Length Required'
- explanation = ('Content-Length header required.')
- class HTTPPreconditionFailed(HTTPClientError):
- code = 412
- title = 'Precondition Failed'
- explanation = ('Request precondition failed.')
- class HTTPRequestEntityTooLarge(HTTPClientError):
- code = 413
- title = 'Request Entity Too Large'
- explanation = ('The body of your request was too large for this server.')
- class HTTPRequestURITooLong(HTTPClientError):
- code = 414
- title = 'Request-URI Too Long'
- explanation = ('The request URI was too long for this server.')
- class HTTPUnsupportedMediaType(HTTPClientError):
- code = 415
- title = 'Unsupported Media Type'
- # override template since we need an environment variable
- template = ('The request media type %(CONTENT_TYPE)s is not '
- 'supported by this server.\r\n%(detail)s')
- class HTTPRequestRangeNotSatisfiable(HTTPClientError):
- code = 416
- title = 'Request Range Not Satisfiable'
- explanation = ('The Range requested is not available.')
- class HTTPExpectationFailed(HTTPClientError):
- code = 417
- title = 'Expectation Failed'
- explanation = ('Expectation failed.')
- #
- # 5xx Server Error
- #
- # Response status codes beginning with the digit "5" indicate cases in
- # which the server is aware that it has erred or is incapable of
- # performing the request. Except when responding to a HEAD request, the
- # server SHOULD include an entity containing an explanation of the error
- # situation, and whether it is a temporary or permanent condition. User
- # agents SHOULD display any included entity to the user. These response
- # codes are applicable to any request method.
- #
- class HTTPServerError(HTTPError):
- """
- base class for the 500's, where the server is in-error
- This is an error condition in which the server is presumed to be
- in-error. This is usually unexpected, and thus requires a traceback;
- ideally, opening a support ticket for the customer. Unless specialized,
- this is a '500 Internal Server Error'
- """
- code = 500
- title = 'Internal Server Error'
- explanation = (
- 'The server has either erred or is incapable of performing\r\n'
- 'the requested operation.\r\n')
- class HTTPInternalServerError(HTTPServerError):
- pass
- class HTTPNotImplemented(HTTPServerError):
- code = 501
- title = 'Not Implemented'
- # override template since we need an environment variable
- template = ('The request method %(REQUEST_METHOD)s is not implemented '
- 'for this server.\r\n%(detail)s')
- class HTTPBadGateway(HTTPServerError):
- code = 502
- title = 'Bad Gateway'
- explanation = ('Bad gateway.')
- class HTTPServiceUnavailable(HTTPServerError):
- code = 503
- title = 'Service Unavailable'
- explanation = ('The server is currently unavailable. '
- 'Please try again at a later time.')
- class HTTPGatewayTimeout(HTTPServerError):
- code = 504
- title = 'Gateway Timeout'
- explanation = ('The gateway has timed out.')
- class HTTPVersionNotSupported(HTTPServerError):
- code = 505
- title = 'HTTP Version Not Supported'
- explanation = ('The HTTP version is not supported.')
- # abstract HTTP related exceptions
- __all__ = ['HTTPException', 'HTTPRedirection', 'HTTPError' ]
- _exceptions = {}
- for name, value in six.iteritems(dict(globals())):
- if (isinstance(value, (type, six.class_types)) and
- issubclass(value, HTTPException) and
- value.code):
- _exceptions[value.code] = value
- __all__.append(name)
- def get_exception(code):
- return _exceptions[code]
- ############################################################
- ## Middleware implementation:
- ############################################################
- class HTTPExceptionHandler(object):
- """
- catches exceptions and turns them into proper HTTP responses
- This middleware catches any exceptions (which are subclasses of
- ``HTTPException``) and turns them into proper HTTP responses.
- Note if the headers have already been sent, the stack trace is
- always maintained as this indicates a programming error.
- Note that you must raise the exception before returning the
- app_iter, and you cannot use this with generator apps that don't
- raise an exception until after their app_iter is iterated over.
- """
- def __init__(self, application, warning_level=None):
- assert not warning_level or ( warning_level > 99 and
- warning_level < 600)
- if warning_level is not None:
- import warnings
- warnings.warn('The warning_level parameter is not used or supported',
- DeprecationWarning, 2)
- self.warning_level = warning_level or 500
- self.application = application
- def __call__(self, environ, start_response):
- environ['paste.httpexceptions'] = self
- environ.setdefault('paste.expected_exceptions',
- []).append(HTTPException)
- try:
- return self.application(environ, start_response)
- except HTTPException as exc:
- return exc(environ, start_response)
- def middleware(*args, **kw):
- import warnings
- # deprecated 13 dec 2005
- warnings.warn('httpexceptions.middleware is deprecated; use '
- 'make_middleware or HTTPExceptionHandler instead',
- DeprecationWarning, 2)
- return make_middleware(*args, **kw)
- def make_middleware(app, global_conf=None, warning_level=None):
- """
- ``httpexceptions`` middleware; this catches any
- ``paste.httpexceptions.HTTPException`` exceptions (exceptions like
- ``HTTPNotFound``, ``HTTPMovedPermanently``, etc) and turns them
- into proper HTTP responses.
- ``warning_level`` can be an integer corresponding to an HTTP code.
- Any code over that value will be passed 'up' the chain, potentially
- reported on by another piece of middleware.
- """
- if warning_level:
- warning_level = int(warning_level)
- return HTTPExceptionHandler(app, warning_level=warning_level)
- __all__.extend(['HTTPExceptionHandler', 'get_exception'])
|