Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/Paste/paste/httpexceptions.py
0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003# (c) 2005 Ian Bicking, Clark C. Evans and contributors
0004# This module is part of the Python Paste Project and is released under
0005# the MIT License: http://www.opensource.org/licenses/mit-license.php
0006# Some of this code was funded by http://prometheusresearch.com
0007"""
0008HTTP Exception Middleware
0009
0010This module processes Python exceptions that relate to HTTP exceptions
0011by defining a set of exceptions, all subclasses of HTTPException, and a
0012request handler (`middleware`) that catches these exceptions and turns
0013them into proper responses.
0014
0015This module defines exceptions according to RFC 2068 [1]_ : codes with
0016100-300 are not really errors; 400's are client errors, and 500's are
0017server errors.  According to the WSGI specification [2]_ , the application
0018can call ``start_response`` more then once only under two conditions:
0019(a) the response has not yet been sent, or (b) if the second and
0020subsequent invocations of ``start_response`` have a valid ``exc_info``
0021argument obtained from ``sys.exc_info()``.  The WSGI specification then
0022requires the server or gateway to handle the case where content has been
0023sent and then an exception was encountered.
0024
0025Exceptions in the 5xx range and those raised after ``start_response``
0026has been called are treated as serious errors and the ``exc_info`` is
0027filled-in with information needed for a lower level module to generate a
0028stack trace and log information.
0029
0030Exception
0031  HTTPException
0032    HTTPRedirection
0033      * 300 - HTTPMultipleChoices
0034      * 301 - HTTPMovedPermanently
0035      * 302 - HTTPFound
0036      * 303 - HTTPSeeOther
0037      * 304 - HTTPNotModified
0038      * 305 - HTTPUseProxy
0039      * 306 - Unused (not implemented, obviously)
0040      * 307 - HTTPTemporaryRedirect
0041    HTTPError
0042      HTTPClientError
0043        * 400 - HTTPBadRequest
0044        * 401 - HTTPUnauthorized
0045        * 402 - HTTPPaymentRequired
0046        * 403 - HTTPForbidden
0047        * 404 - HTTPNotFound
0048        * 405 - HTTPMethodNotAllowed
0049        * 406 - HTTPNotAcceptable
0050        * 407 - HTTPProxyAuthenticationRequired
0051        * 408 - HTTPRequestTimeout
0052        * 409 - HTTPConfict
0053        * 410 - HTTPGone
0054        * 411 - HTTPLengthRequired
0055        * 412 - HTTPPreconditionFailed
0056        * 413 - HTTPRequestEntityTooLarge
0057        * 414 - HTTPRequestURITooLong
0058        * 415 - HTTPUnsupportedMediaType
0059        * 416 - HTTPRequestRangeNotSatisfiable
0060        * 417 - HTTPExpectationFailed
0061      HTTPServerError
0062        * 500 - HTTPInternalServerError
0063        * 501 - HTTPNotImplemented
0064        * 502 - HTTPBadGateway
0065        * 503 - HTTPServiceUnavailable
0066        * 504 - HTTPGatewayTimeout
0067        * 505 - HTTPVersionNotSupported
0068
0069References:
0070
0071.. [1] http://www.python.org/peps/pep-0333.html#error-handling
0072.. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
0073
0074"""
0075
0076import types
0077from paste.wsgilib import catch_errors_app
0078from paste.response import has_header, header_value, replace_header
0079from paste.request import resolve_relative_url
0080from paste.util.quoting import strip_html, html_quote, no_quote
0081
0082SERVER_NAME = 'WSGI Server'
0083TEMPLATE = """\
0084<html>\r
0085  <head><title>%(title)s</title></head>\r
0086  <body>\r
0087    <h1>%(title)s</h1>\r
0088    <p>%(body)s</p>\r
0089    <hr noshade>\r
0090    <div align="right">%(server)s</div>\r
0091  </body>\r
0092</html>\r
0093"""
0094
0095class HTTPException(Exception):
0096    """
0097    the HTTP exception base class
0098
0099    This encapsulates an HTTP response that interrupts normal application
0100    flow; but one which is not necessarly an error condition. For
0101    example, codes in the 300's are exceptions in that they interrupt
0102    normal processing; however, they are not considered errors.
0103
0104    This class is complicated by 4 factors:
0105
0106      1. The content given to the exception may either be plain-text or
0107         as html-text.
0108
0109      2. The template may want to have string-substitutions taken from
0110         the current ``environ`` or values from incoming headers. This
0111         is especially troublesome due to case sensitivity.
0112
0113      3. The final output may either be text/plain or text/html
0114         mime-type as requested by the client application.
0115
0116      4. Each exception has a default explanation, but those who
0117         raise exceptions may want to provide additional detail.
0118
0119    Attributes:
0120
0121       ``code``
0122           the HTTP status code for the exception
0123
0124       ``title``
0125           remainder of the status line (stuff after the code)
0126
0127       ``explanation``
0128           a plain-text explanation of the error message that is
0129           not subject to environment or header substitutions;
0130           it is accessible in the template via %(explanation)s
0131
0132       ``detail``
0133           a plain-text message customization that is not subject
0134           to environment or header substitutions; accessible in
0135           the template via %(detail)s
0136
0137       ``template``
0138           a content fragment (in HTML) used for environment and
0139           header substitution; the default template includes both
0140           the explanation and further detail provided in the
0141           message
0142
0143       ``required_headers``
0144           a sequence of headers which are required for proper
0145           construction of the exception
0146
0147    Parameters:
0148
0149       ``detail``
0150         a plain-text override of the default ``detail``
0151
0152       ``headers``
0153         a list of (k,v) header pairs
0154
0155       ``comment``
0156         a plain-text additional information which is
0157         usually stripped/hidden for end-users
0158
0159    To override the template (which is HTML content) or the plain-text
0160    explanation, one must subclass the given exception; or customize it
0161    after it has been created.  This particular breakdown of a message
0162    into explanation, detail and template allows both the creation of
0163    plain-text and html messages for various clients as well as
0164    error-free substitution of environment variables and headers.
0165    """
0166
0167    code = None
0168    title = None
0169    explanation = ''
0170    detail = ''
0171    comment = ''
0172    template = "%(explanation)s\r\n<br/>%(detail)s\r\n<!-- %(comment)s -->"
0173    required_headers = ()
0174
0175    def __init__(self, detail=None, headers=None, comment=None):
0176        assert self.code, "Do not directly instantiate abstract exceptions."
0177        assert isinstance(headers, (type(None), list)), (
0178            "headers must be None or a list: %r"
0179            % headers)
0180        assert isinstance(detail, (type(None), basestring)), (
0181            "detail must be None or a string: %r" % detail)
0182        assert isinstance(comment, (type(None), basestring)), (
0183            "comment must be None or a string: %r" % comment)
0184        self.headers = headers or tuple()
0185        for req in self.required_headers:
0186            assert headers and has_header(headers, req), (
0187                "Exception %s must be passed the header %r "
0188                "(got headers: %r)"
0189                % (self.__class__.__name__, req, headers))
0190        if detail is not None:
0191            self.detail = detail
0192        if comment is not None:
0193            self.comment = comment
0194        Exception.__init__(self,"%s %s\n%s\n%s\n" % (
0195            self.code, self.title, self.explanation, self.detail))
0196
0197    def make_body(self, environ, template, escfunc, comment_escfunc=None):
0198        comment_escfunc = comment_escfunc or escfunc
0199        args = {'explanation': escfunc(self.explanation),
0200                'detail': escfunc(self.detail),
0201                'comment': comment_escfunc(self.comment)}
0202        if HTTPException.template == self.template:
0203            return template % args
0204        for (k, v) in environ.items():
0205            args[k] = escfunc(v)
0206        if self.headers:
0207            for (k, v) in self.headers:
0208                args[k.lower()] = escfunc(v)
0209        return template % args
0210
0211    def plain(self, environ):
0212        """ text/plain representation of the exception """
0213        body = self.make_body(environ, strip_html(self.template), no_quote)
0214        return ('%s %s\r\n%s\r\n' % (self.code, self.title, body))
0215
0216    def html(self, environ):
0217        """ text/html representation of the exception """
0218        body = self.make_body(environ, self.template, html_quote, no_quote)
0219        return TEMPLATE % {
0220                   'title': self.title,
0221                   'code': self.code,
0222                   'server': SERVER_NAME,
0223                   'body': body }
0224
0225    def prepare_content(self, environ):
0226        if self.headers:
0227            headers = list(self.headers)
0228        else:
0229            headers = []
0230        if 'html' in environ.get('HTTP_ACCEPT','') or               '*/*' in environ.get('HTTP_ACCEPT',''):
0232            replace_header(headers, 'content-type', 'text/html')
0233            content = self.html(environ)
0234        else:
0235            replace_header(headers, 'content-type', 'text/plain')
0236            content = self.plain(environ)
0237        if isinstance(content, unicode):
0238            content = content.encode('utf8')
0239            cur_content_type = (
0240                header_value(headers, 'content-type')
0241                or 'text/html')
0242            replace_header(
0243                headers, 'content-type',
0244                cur_content_type + '; charset=utf8')
0245        return headers, content
0246
0247    def response(self, environ):
0248        from paste.wsgiwrappers import WSGIResponse
0249        headers, content = self.prepare_content(environ)
0250        resp = WSGIResponse(code=self.code, content=content)
0251        resp.headers = resp.headers.fromlist(headers)
0252        return resp
0253
0254    def wsgi_application(self, environ, start_response, exc_info=None):
0255        """
0256        This exception as a WSGI application
0257        """
0258        headers, content = self.prepare_content(environ)
0259        start_response('%s %s' % (self.code, self.title),
0260                       headers,
0261                       exc_info)
0262        return [content]
0263
0264    __call__ = wsgi_application
0265
0266    def __repr__(self):
0267        return '<%s %s; code=%s>' % (self.__class__.__name__,
0268                                     self.title, self.code)
0269
0270class HTTPError(HTTPException):
0271    """
0272    base class for status codes in the 400's and 500's
0273
0274    This is an exception which indicates that an error has occurred,
0275    and that any work in progress should not be committed.  These are
0276    typically results in the 400's and 500's.
0277    """
0278
0279#
0280# 3xx Redirection
0281#
0282#  This class of status code indicates that further action needs to be
0283#  taken by the user agent in order to fulfill the request. The action
0284#  required MAY be carried out by the user agent without interaction with
0285#  the user if and only if the method used in the second request is GET or
0286#  HEAD. A client SHOULD detect infinite redirection loops, since such
0287#  loops generate network traffic for each redirection.
0288#
0289
0290class HTTPRedirection(HTTPException):
0291    """
0292    base class for 300's status code (redirections)
0293
0294    This is an abstract base class for 3xx redirection.  It indicates
0295    that further action needs to be taken by the user agent in order
0296    to fulfill the request.  It does not necessarly signal an error
0297    condition.
0298    """
0299
0300class _HTTPMove(HTTPRedirection):
0301    """
0302    redirections which require a Location field
0303
0304    Since a 'Location' header is a required attribute of 301, 302, 303,
0305    305 and 307 (but not 304), this base class provides the mechanics to
0306    make this easy.  While this has the same parameters as HTTPException,
0307    if a location is not provided in the headers; it is assumed that the
0308    detail _is_ the location (this for backward compatibility, otherwise
0309    we'd add a new attribute).
0310    """
0311    required_headers = ('location',)
0312    explanation = 'The resource has been moved to'
0313    template = (
0314        '%(explanation)s <a href="%(location)s">%(location)s</a>;\r\n'
0315        'you should be redirected automatically.\r\n'
0316        '%(detail)s\r\n<!-- %(comment)s -->')
0317
0318    def __init__(self, detail=None, headers=None, comment=None):
0319        assert isinstance(headers, (type(None), list))
0320        headers = headers or []
0321        location = header_value(headers,'location')
0322        if not location:
0323            location = detail
0324            detail = ''
0325            headers.append(('location', location))
0326        assert location, ("HTTPRedirection specified neither a "
0327                          "location in the headers nor did it "
0328                          "provide a detail argument.")
0329        HTTPRedirection.__init__(self, location, headers, comment)
0330        if detail is not None:
0331            self.detail = detail
0332
0333    def relative_redirect(cls, dest_uri, environ, detail=None, headers=None, comment=None):
0334        """
0335        Create a redirect object with the dest_uri, which may be relative, 
0336        considering it relative to the uri implied by the given environ.
0337        """
0338        location = resolve_relative_url(dest_uri, environ)
0339        headers = headers or []
0340        headers.append(('Location', location))
0341        return cls(detail=detail, headers=headers, comment=comment)
0342
0343    relative_redirect = classmethod(relative_redirect)
0344
0345    def location(self):
0346        for name, value in self.headers:
0347            if name.lower() == 'location':
0348                return value
0349        else:
0350            raise KeyError("No location set for %s" % self)
0351
0352class HTTPMultipleChoices(_HTTPMove):
0353    code = 300
0354    title = 'Multiple Choices'
0355
0356class HTTPMovedPermanently(_HTTPMove):
0357    code = 301
0358    title = 'Moved Permanently'
0359
0360class HTTPFound(_HTTPMove):
0361    code = 302
0362    title = 'Found'
0363    explanation = 'The resource was found at'
0364
0365# This one is safe after a POST (the redirected location will be
0366# retrieved with GET):
0367class HTTPSeeOther(_HTTPMove):
0368    code = 303
0369    title = 'See Other'
0370
0371class HTTPNotModified(HTTPRedirection):
0372    # @@: but not always (HTTP section 14.18.1)...?
0373    # @@: Removed 'date' requirement, as its not required for an ETag
0374    # @@: FIXME: This should require either an ETag or a date header
0375    code = 304
0376    title = 'Not Modified'
0377    message = ''
0378    # @@: should include date header, optionally other headers
0379    # @@: should not return a content body
0380    def plain(self, environ):
0381        return ''
0382    def html(self, environ):
0383        """ text/html representation of the exception """
0384        return ''
0385
0386class HTTPUseProxy(_HTTPMove):
0387    # @@: OK, not a move, but looks a little like one
0388    code = 305
0389    title = 'Use Proxy'
0390    explanation = (
0391        'The resource must be accessed through a proxy '
0392        'located at')
0393
0394class HTTPTemporaryRedirect(_HTTPMove):
0395    code = 307
0396    title = 'Temporary Redirect'
0397
0398#
0399# 4xx Client Error
0400#
0401#  The 4xx class of status code is intended for cases in which the client
0402#  seems to have erred. Except when responding to a HEAD request, the
0403#  server SHOULD include an entity containing an explanation of the error
0404#  situation, and whether it is a temporary or permanent condition. These
0405#  status codes are applicable to any request method. User agents SHOULD
0406#  display any included entity to the user.
0407#
0408
0409class HTTPClientError(HTTPError):
0410    """
0411    base class for the 400's, where the client is in-error
0412
0413    This is an error condition in which the client is presumed to be
0414    in-error.  This is an expected problem, and thus is not considered
0415    a bug.  A server-side traceback is not warranted.  Unless specialized,
0416    this is a '400 Bad Request'
0417    """
0418    code = 400
0419    title = 'Bad Request'
0420    explanation = ('The server could not comply with the request since\r\n'
0421                   'it is either malformed or otherwise incorrect.\r\n')
0422
0423class HTTPBadRequest(HTTPClientError):
0424    pass
0425
0426class HTTPUnauthorized(HTTPClientError):
0427    code = 401
0428    title = 'Unauthorized'
0429    explanation = (
0430        'This server could not verify that you are authorized to\r\n'
0431        'access the document you requested.  Either you supplied the\r\n'
0432        'wrong credentials (e.g., bad password), or your browser\r\n'
0433        'does not understand how to supply the credentials required.\r\n')
0434
0435class HTTPPaymentRequired(HTTPClientError):
0436    code = 402
0437    title = 'Payment Required'
0438    explanation = ('Access was denied for financial reasons.')
0439
0440class HTTPForbidden(HTTPClientError):
0441    code = 403
0442    title = 'Forbidden'
0443    explanation = ('Access was denied to this resource.')
0444
0445class HTTPNotFound(HTTPClientError):
0446    code = 404
0447    title = 'Not Found'
0448    explanation = ('The resource could not be found.')
0449
0450class HTTPMethodNotAllowed(HTTPClientError):
0451    required_headers = ('allow',)
0452    code = 405
0453    title = 'Method Not Allowed'
0454    # override template since we need an environment variable
0455    template = ('The method %(REQUEST_METHOD)s is not allowed for '
0456                'this resource.\r\n%(detail)s')
0457
0458class HTTPNotAcceptable(HTTPClientError):
0459    code = 406
0460    title = 'Not Acceptable'
0461    # override template since we need an environment variable
0462    template = ('The resource could not be generated that was '
0463                'acceptable to your browser (content\r\nof type '
0464                '%(HTTP_ACCEPT)s).\r\n%(detail)s')
0465
0466class HTTPProxyAuthenticationRequired(HTTPClientError):
0467    code = 407
0468    title = 'Proxy Authentication Required'
0469    explanation = ('Authentication /w a local proxy is needed.')
0470
0471class HTTPRequestTimeout(HTTPClientError):
0472    code = 408
0473    title = 'Request Timeout'
0474    explanation = ('The server has waited too long for the request to '
0475                   'be sent by the client.')
0476
0477class HTTPConflict(HTTPClientError):
0478    code = 409
0479    title = 'Conflict'
0480    explanation = ('There was a conflict when trying to complete '
0481                   'your request.')
0482
0483class HTTPGone(HTTPClientError):
0484    code = 410
0485    title = 'Gone'
0486    explanation = ('This resource is no longer available.  No forwarding '
0487                   'address is given.')
0488
0489class HTTPLengthRequired(HTTPClientError):
0490    code = 411
0491    title = 'Length Required'
0492    explanation = ('Content-Length header required.')
0493
0494class HTTPPreconditionFailed(HTTPClientError):
0495    code = 412
0496    title = 'Precondition Failed'
0497    explanation = ('Request precondition failed.')
0498
0499class HTTPRequestEntityTooLarge(HTTPClientError):
0500    code = 413
0501    title = 'Request Entity Too Large'
0502    explanation = ('The body of your request was too large for this server.')
0503
0504class HTTPRequestURITooLong(HTTPClientError):
0505    code = 414
0506    title = 'Request-URI Too Long'
0507    explanation = ('The request URI was too long for this server.')
0508
0509class HTTPUnsupportedMediaType(HTTPClientError):
0510    code = 415
0511    title = 'Unsupported Media Type'
0512    # override template since we need an environment variable
0513    template = ('The request media type %(CONTENT_TYPE)s is not '
0514                'supported by this server.\r\n%(detail)s')
0515
0516class HTTPRequestRangeNotSatisfiable(HTTPClientError):
0517    code = 416
0518    title = 'Request Range Not Satisfiable'
0519    explanation = ('The Range requested is not available.')
0520
0521class HTTPExpectationFailed(HTTPClientError):
0522    code = 417
0523    title = 'Expectation Failed'
0524    explanation = ('Expectation failed.')
0525
0526#
0527# 5xx Server Error
0528#
0529#  Response status codes beginning with the digit "5" indicate cases in
0530#  which the server is aware that it has erred or is incapable of
0531#  performing the request. Except when responding to a HEAD request, the
0532#  server SHOULD include an entity containing an explanation of the error
0533#  situation, and whether it is a temporary or permanent condition. User
0534#  agents SHOULD display any included entity to the user. These response
0535#  codes are applicable to any request method.
0536#
0537
0538class HTTPServerError(HTTPError):
0539    """
0540    base class for the 500's, where the server is in-error
0541
0542    This is an error condition in which the server is presumed to be
0543    in-error.  This is usually unexpected, and thus requires a traceback;
0544    ideally, opening a support ticket for the customer. Unless specialized,
0545    this is a '500 Internal Server Error'
0546    """
0547    code = 500
0548    title = 'Internal Server Error'
0549    explanation = (
0550      'The server has either erred or is incapable of performing\r\n'
0551      'the requested operation.\r\n')
0552
0553class HTTPInternalServerError(HTTPServerError):
0554    pass
0555
0556class HTTPNotImplemented(HTTPServerError):
0557    code = 501
0558    title = 'Not Implemented'
0559    # override template since we need an environment variable
0560    template = ('The request method %(REQUEST_METHOD)s is not implemented '
0561                'for this server.\r\n%(detail)s')
0562
0563class HTTPBadGateway(HTTPServerError):
0564    code = 502
0565    title = 'Bad Gateway'
0566    explanation = ('Bad gateway.')
0567
0568class HTTPServiceUnavailable(HTTPServerError):
0569    code = 503
0570    title = 'Service Unavailable'
0571    explanation = ('The server is currently unavailable. '
0572                   'Please try again at a later time.')
0573
0574class HTTPGatewayTimeout(HTTPServerError):
0575