diff options
author | Marc Abramowitz <marc@marc-abramowitz.com> | 2016-03-07 14:05:52 -0800 |
---|---|---|
committer | Marc Abramowitz <marc@marc-abramowitz.com> | 2016-03-07 14:05:52 -0800 |
commit | 42b22881290e00e06b840dee1e42f0f5ef044d47 (patch) | |
tree | b4fef928625acd3e8ee45ccaa8c7a6c9810b3601 /paste/httpexceptions.py | |
download | paste-git-tox_add_py35.tar.gz |
tox.ini: Add py35 to envlisttox_add_py35
Diffstat (limited to 'paste/httpexceptions.py')
-rw-r--r-- | paste/httpexceptions.py | 667 |
1 files changed, 667 insertions, 0 deletions
diff --git a/paste/httpexceptions.py b/paste/httpexceptions.py new file mode 100644 index 0000000..0b68c2d --- /dev/null +++ b/paste/httpexceptions.py @@ -0,0 +1,667 @@ +# (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 + * 429 - HTTPTooManyRequests + 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.') + +class HTTPTooManyRequests(HTTPClientError): + code = 429 + title = 'Too Many Requests' + explanation = ('The client has sent too many requests to the server.') + +# +# 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']) |