diff options
author | ianb <devnull@localhost> | 2005-12-13 07:00:20 +0000 |
---|---|---|
committer | ianb <devnull@localhost> | 2005-12-13 07:00:20 +0000 |
commit | 4e73bff9da87e35c7154ab1cc923bb4f9d40711d (patch) | |
tree | d2e4c92965398700457280d5829dfaa5cdf5b4fb /paste/httpexceptions.py | |
parent | 55b404e53bc834daf3852069af6de9b1fca4c742 (diff) | |
download | paste-4e73bff9da87e35c7154ab1cc923bb4f9d40711d.tar.gz |
Merged changes from cce branch (r3727:HEAD/4008); the branch is now in sync with trunk
Diffstat (limited to 'paste/httpexceptions.py')
-rw-r--r-- | paste/httpexceptions.py | 539 |
1 files changed, 428 insertions, 111 deletions
diff --git a/paste/httpexceptions.py b/paste/httpexceptions.py index 2378b2c..c38618c 100644 --- a/paste/httpexceptions.py +++ b/paste/httpexceptions.py @@ -1,57 +1,287 @@ -# (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 """ -WSGI middleware - -Processes Python exceptions that relate to HTTP exceptions. This -defines a set of extensions, all subclasses of HTTPException, and a -middleware (`middleware`) that catches these exceptions and turns them -into proper responses. - -Note: if ``'paste.debug_suppress_httpexceptions'`` is in the request -and is true, then this middleware will be skipped. +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. + +References: +[1] http://www.python.org/peps/pep-0333.html#error-handling +[2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5 + +Exception + HTTPException + HTTPRedirection + # 300 Multiple Choices + 301 - HTTPMovedPermanently + 302 - HTTPFound + 303 - HTTPSeeOther + 304 - HTTPNotModified + 305 - HTTPUseProxy + # 306 Unused + 307 - HTTPTemporaryRedirect + HTTPError + HTTPClientError + 400 - HTTPBadRequest + 401 - HTTPUnauthorized + # 402 Payment Required + 403 - HTTPForbidden + 404 - HTTPNotFound + 405 - HTTPMethodNotAllowed + 406 - HTTPNotAcceptable + # 407 Proxy Authentication Required + # 408 Request Timeout + 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 """ import types +import sys +from wsgilib import has_header, header_value +from util.quoting import strip_html, html_quote class HTTPException(Exception): + """ + Base class for all HTTP exceptions + + 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 accessable in the template via %(explanation)s + + ``detail`` + a plain-text message customization that is not subject + to environment or header substutions; accessable in + the template via %(detail)s + + ``template`` + a content fragment (in HTML) used for environment and + header substution; 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 substution of environment variables and headers. + """ + code = None title = None - message = None - # @@: not currently used: + explanation = '' + detail = '' + comment = '' + template = "%(explanation)s\n<br/>%(detail)s\n<!-- %(comment)s -->" required_headers = () - def __init__(self, message=None, headers=None): - self.headers = headers - if message is not None: - self.message = message - Exception.__init__(self, self.message) + server_name = 'WSGI server' + + def __init__(self, detail=None, headers=None, comment=None): + assert self.code, "Do not directly instantiate abstract exceptions." + assert isinstance(headers, (type(None), list)) + assert isinstance(detail, (type(None), basestring)) + assert isinstance(comment, (type(None), basestring)) + self.headers = headers or tuple() + for req in self.required_headers: + assert has_header(headers, req) + 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): + args = {'explanation': escfunc(self.explanation), + 'detail': escfunc(self.detail), + 'comment': escfunc(self.comment)} + if HTTPException.template == self.template: + return template % args + for (k, v) in environ.items(): + args[k] = escfunc(v) + if self.headers: + for (k, v) in self.headers: + args[k.lower()] = escfunc(v) + return template % args + + def plain(self, environ): + """ text/plain representation of the exception """ + noop = lambda _: _ + body = self.make_body(environ, strip_html(self.template), noop) + return ('%s %s\n%s\n' % (self.code, self.title, body)) def html(self, environ): - message = self.message - args = environ.copy() - if self.headers: - args.update(self.headers) - message = message % args + """ text/html representation of the exception """ + body = self.make_body(environ, self.template, html_quote) return ('<html><head><title>%(title)s</title></head>\n' '<body>\n' '<h1>%(title)s</h1>\n' - '<p>%(message)s</p>\n' + '<p>%(body)s</p>\n' '<hr noshade>\n' - '<div align="right">WSGI server</div>\n' + '<div align="right">%(server)s</div>\n' '</body></html>\n' % {'title': self.title, 'code': self.code, - 'message': message}) + 'server': self.server_name, + 'body': body}) + + def wsgi_application(self, environ, start_response, exc_info=None): + """ + This exception as a WSGI application + """ + if 'html' in environ.get('HTTP_ACCEPT',''): + headers = {'content-type': 'text/html'} + content = self.html(environ) + else: + headers = {'content-type': 'text/plain'} + content = self.plain(environ) + if self.headers: + headers.update(self.headers) + if isinstance(content, unicode): + content = content.encode('utf8') + headers['content_type'] += '; charset=utf8' + start_response('%s %s' % (self.code, self.title), + headers.items(), + exc_info) + yield content + def __repr__(self): return '<%s %s; code=%s>' % (self.__class__.__name__, self.title, self.code) -class _HTTPMove(HTTPException): +class HTTPError(HTTPException): + """ + This is an exception which indicates that an error has occured, + 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): + """ + 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): + """ + Base class for 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',) - message = ('The resource has been moved to <a href="%(location)s">' - '%(location)s</a>; you should be redirected automatically.') + explanation = 'The resource has been moved to' + template = ( + '%(explanation)s <a href="%(location)s">%(location)s</a>;\n' + 'you should be redirected automatically.\n' + '%(detail)s\n') + + 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 class HTTPMovedPermanently(_HTTPMove): code = 301 @@ -60,6 +290,7 @@ class HTTPMovedPermanently(_HTTPMove): 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): @@ -67,142 +298,206 @@ class HTTPSeeOther(_HTTPMove): code = 303 title = 'See Other' -class HTTPNotModified(HTTPException): +class HTTPNotModified(HTTPRedirection): # @@: but not always (HTTP section 14.18.1)...? required_headers = ('date',) 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' - message = ('This resource must be accessed through the proxy located ' - 'at <a href="%(location)s">%(location)s</a>') + explanation = ( + 'The resource must be accessed through a proxy ' + 'located at') class HTTPTemporaryRedirect(_HTTPMove): code = 307 title = 'Temporary Redirect' -class HTTPBadRequest(HTTPException): +# +# 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): + """ + 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' - message = ('The server could not understand your request') + explanation = 'The server could not understand your request.' + +HTTPBadRequest = HTTPClientError -class HTTPUnauthorized(HTTPException): +class HTTPUnauthorized(HTTPClientError): required_headers = ('WWW-Authenticate',) code = 401 title = 'Unauthorized' - # @@: should require WWW-Authenticate header - message = ('Authorization is required to access this resource; ' - 'you must login.') + explanation = ( + 'This server could not verify that you are authorized to\n' + 'access the document you requested. Either you supplied the\n' + 'wrong credentials (e.g., bad password), or your browser\n' + 'does not understand how to supply the credentials required.\n') -class HTTPForbidden(HTTPException): +class HTTPForbidden(HTTPClientError): code = 403 title = 'Forbidden' - message = ('Access was denied to this resource.') + explanation = ('Access was denied to this resource.') -class HTTPNotFound(HTTPException): +class HTTPNotFound(HTTPClientError): code = 404 title = 'Not Found' - message = ('The resource could not be found.') + explanation = ('The resource could not be found.') -class HTTPMethodNotAllowed(HTTPException): +class HTTPMethodNotAllowed(HTTPClientError): required_headers = ('allowed',) code = 405 title = 'Method Not Allowed' - message = ('The method %(REQUEST_METHOD)s is not allowed for this ' - 'resource.') + # override template since we need an environment variable + template = ('The method %(REQUEST_METHOD)s is not allowed for ' + 'this resource.\n%(detail)s') -class HTTPNotAcceptable(HTTPException): +class HTTPNotAcceptable(HTTPClientError): code = 406 title = 'Not Acceptable' - message = ('The resource could not be generated that was acceptable ' - 'to your browser (content of type %(HTTP_ACCEPT)s).') + # override template since we need an environment variable + template = ('The resource could not be generated that was ' + 'acceptable to your browser (content\nof type ' + '%(HTTP_ACCEPT)s).\n%(detail)s') +<<<<<<< .working class HTTPConflict(HTTPException): +======= +class HTTPConflict(HTTPClientError): +>>>>>>> .merge-right.r4008 code = 409 title = 'Conflict' - message = ('There was a conflict when trying to complete your ' - 'request.') + explanation = ('There was a conflict when trying to complete ' + 'your request.') -class HTTPGone(HTTPException): +class HTTPGone(HTTPClientError): code = 410 title = 'Gone' - message = ('This resource is no longer available. No forwarding ' - 'address is aavailable.') + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') -class HTTPLengthRequired(HTTPException): +class HTTPLengthRequired(HTTPClientError): code = 411 title = 'Length Required' - message = ('Content-Length header required.') + explanation = ('Content-Length header required.') -class HTTPPreconditionFailed(HTTPException): +class HTTPPreconditionFailed(HTTPClientError): code = 412 title = 'Precondition Failed' - message = ('Request precondition failed.') + explanation = ('Request precondition failed.') -class HTTPRequestEntityTooLarge(HTTPException): +class HTTPRequestEntityTooLarge(HTTPClientError): code = 413 title = 'Request Entity Too Large' - message = ('The body of your request was too large for this server.') + explanation = ('The body of your request was too large for this server.') -class HTTPRequestURITooLong(HTTPException): +class HTTPRequestURITooLong(HTTPClientError): code = 414 title = 'Request-URI Too Long' - message = ('The request URI was too long for this server.') + explanation = ('The request URI was too long for this server.') -class HTTPUnsupportedMediaType(HTTPException): +class HTTPUnsupportedMediaType(HTTPClientError): code = 415 title = 'Unsupported Media Type' - message = ('The request media type %(CONTENT_TYPE)s is not ' - 'supported by this server.') + # override template since we need an environment variable + template = ('The request media type %(CONTENT_TYPE)s is not ' + 'supported by this server.\n%(detail)s') -class HTTPRequestRangeNotSatisfiable(HTTPException): +class HTTPRequestRangeNotSatisfiable(HTTPClientError): code = 416 title = 'Request Range Not Satisfiable' - message = ('The Range requested is not available.') + explanation = ('The Range requested is not available.') -class HTTPExpectationFailed(HTTPException): +class HTTPExpectationFailed(HTTPClientError): code = 417 title = 'Expectation Failed' - message = ('Expectation failed.') - -class HTTPServerError(HTTPException): + 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): + """ + 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' - message = ('An internal server error occurred.') + explanation = ('An internal server error occurred.') +<<<<<<< .working class HTTPNotImplemented(HTTPException): code = 501 +======= +HTTPInternalServerError = HTTPServerError + +class HTTPNotImplemented(HTTPServerError): + code = 501 +>>>>>>> .merge-right.r4008 title = 'Not Implemented' - message = ('The request method %(REQUEST_METHOD)s is not implemented ' - 'for this server.') + # override template since we need an environment variable + template = ('The request method %(REQUEST_METHOD)s is not implemented ' + 'for this server.\n%(detail)s') -class HTTPBadGateway(HTTPException): +class HTTPBadGateway(HTTPServerError): code = 502 title = 'Bad Gateway' - message = ('Bad gateway.') + explanation = ('Bad gateway.') -class HTTPServiceUnavailable(HTTPException): +class HTTPServiceUnavailable(HTTPServerError): code = 503 title = 'Service Unavailable' - message = ('The server is currently unavailable. Please try again ' - 'at a later time.') + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') -class HTTPGatewayTimeout(HTTPException): +class HTTPGatewayTimeout(HTTPServerError): code = 504 title = 'Gateway Timeout' - message = ('The gateway has timed out.') + explanation = ('The gateway has timed out.') -class HTTPHttpVersionNotSupported(HTTPException): +class HTTPVersionNotSupported(HTTPServerError): code = 505 title = 'HTTP Version Not Supported' - message = ('The HTTP version is not supported.') + explanation = ('The HTTP version is not supported.') + +# abstract HTTP related exceptions +__all__ = ['HTTPException', 'HTTPRedirection', 'HTTPError' ] -__all__ = [] _exceptions = {} for name, value in globals().items(): if (isinstance(value, (type, types.ClassType)) and @@ -210,6 +505,7 @@ for name, value in globals().items(): value.code): _exceptions[value.code] = value __all__.append(name) + def get_exception(code): return _exceptions[code] @@ -217,40 +513,61 @@ def get_exception(code): ## Middleware implementation: ############################################################ -def middleware(application, global_conf=None): - +class HTTPExceptionHandler: """ This middleware catches any exceptions (which are subclasses of - `HTTPException`) and turns them into proper HTTP responses. + ``HTTPException``) and turns them into proper HTTP responses. + + Attributes: + + ``warning_level`` + This attribute determines for what exceptions a stack + trace is kept for lower level reporting; by default, it + only keeps stack trace for 5xx, HTTPServerError exceptions. + To keep a stack trace for 4xx, HTTPClientError exceptions, + set this to 400. + + + + Note if the headers have already been sent, the stack trace is + always maintained as this indicates a programming error. + """ - def start_application(environ, start_response): - environ.setdefault('paste.expected_exceptions', []).append( - HTTPException) - app_started = [] - def checked_start_response(status, headers, exc_info=None): - app_started.append(None) + def __init__(self, application, global_conf=None, warning_level=None): + assert not warning_level or ( warning_level > 99 and + warning_level < 600) + 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) + headers_sent = [] + def httpexce_start_response(status, headers, exc_info = None): + headers_sent.append(True) return start_response(status, headers, exc_info) - try: - v = application(environ, checked_start_response) - environ['paste.expected_exceptions'].remove(HTTPException) - return v + result = self.application(environ, httpexce_start_response) + for chunk in result: + yield chunk except HTTPException, e: if environ.get('paste.debug_suppress_httpexceptions'): raise - if app_started: - # They've already started the response, so we can't - # do the right thing anymore. - raise - headers = {'content-type': 'text/html'} - if e.headers: - headers.update(e.headers) - start_response('%s %s' % (e.code, e.title), - headers.items()) - return [e.html(environ)] - - return start_application - -__all__.extend(['middleware', 'get_exception']) + if headers_sent or e.code >= self.warning_level: + exc_info = sys.exc_info() + else: + exc_info = None + try: + result = e.wsgi_application(environ, start_response, exc_info) + finally: + # clean up + exc_info = None + for chunk in result: + yield chunk + +middleware = HTTPExceptionHandler + +__all__.extend(['HTTPExceptionHandler', 'get_exception']) |