0001"""
0002HTTP Exception
0003
0004This module processes Python exceptions that relate to HTTP exceptions
0005by defining a set of exceptions, all subclasses of HTTPException.
0006Each exception, in addition to being a Python exception that can be
0007raised and caught, is also a WSGI application and ``webob.Response``
0008object.
0009
0010This module defines exceptions according to RFC 2068 [1]_ : codes with
0011100-300 are not really errors; 400's are client errors, and 500's are
0012server errors.  According to the WSGI specification [2]_ , the application
0013can call ``start_response`` more then once only under two conditions:
0014(a) the response has not yet been sent, or (b) if the second and
0015subsequent invocations of ``start_response`` have a valid ``exc_info``
0016argument obtained from ``sys.exc_info()``.  The WSGI specification then
0017requires the server or gateway to handle the case where content has been
0018sent and then an exception was encountered.
0019
0020Exception
0021  HTTPException
0022    HTTPOk
0023      * 200 - HTTPOk
0024      * 201 - HTTPCreated
0025      * 202 - HTTPAccepted
0026      * 203 - HTTPNonAuthoritativeInformation
0027      * 204 - HTTPNoContent
0028      * 205 - HTTPResetContent
0029      * 206 - HTTPPartialContent
0030    HTTPRedirection
0031      * 300 - HTTPMultipleChoices
0032      * 301 - HTTPMovedPermanently
0033      * 302 - HTTPFound
0034      * 303 - HTTPSeeOther
0035      * 304 - HTTPNotModified
0036      * 305 - HTTPUseProxy
0037      * 306 - Unused (not implemented, obviously)
0038      * 307 - HTTPTemporaryRedirect
0039    HTTPError
0040      HTTPClientError
0041        * 400 - HTTPBadRequest
0042        * 401 - HTTPUnauthorized
0043        * 402 - HTTPPaymentRequired
0044        * 403 - HTTPForbidden
0045        * 404 - HTTPNotFound
0046        * 405 - HTTPMethodNotAllowed
0047        * 406 - HTTPNotAcceptable
0048        * 407 - HTTPProxyAuthenticationRequired
0049        * 408 - HTTPRequestTimeout
0050        * 409 - HTTPConfict
0051        * 410 - HTTPGone
0052        * 411 - HTTPLengthRequired
0053        * 412 - HTTPPreconditionFailed
0054        * 413 - HTTPRequestEntityTooLarge
0055        * 414 - HTTPRequestURITooLong
0056        * 415 - HTTPUnsupportedMediaType
0057        * 416 - HTTPRequestRangeNotSatisfiable
0058        * 417 - HTTPExpectationFailed
0059      HTTPServerError
0060        * 500 - HTTPInternalServerError
0061        * 501 - HTTPNotImplemented
0062        * 502 - HTTPBadGateway
0063        * 503 - HTTPServiceUnavailable
0064        * 504 - HTTPGatewayTimeout
0065        * 505 - HTTPVersionNotSupported
0066
0067References:
0068
0069.. [1] http://www.python.org/peps/pep-0333.html#error-handling
0070.. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5
0071
0072
0073"""
0074
0075import re
0076import urlparse
0077import sys
0078try:
0079    from string import Template
0080except ImportError:
0081    from webob.util.stringtemplate import Template
0082import types
0083from webob import Response, Request, html_escape
0084
0085tag_re = re.compile(r'<.*?>', re.S)
0086br_re = re.compile(r'<br.*?>', re.I|re.S)
0087comment_re = re.compile(r'<!--|-->')
0088
0089def no_escape(value):
0090    if value is None:
0091        return ''
0092    if not isinstance(value, basestring):
0093        if hasattr(value, '__unicode__'):
0094            value = unicode(value)
0095        else:
0096            value = str(value)
0097    return value
0098
0099def strip_tags(value):
0100    value = value.replace('\n', ' ')
0101    value = value.replace('\r', '')
0102    value = br_re.sub('\n', value)
0103    value = comment_re.sub('', value)
0104    value = tag_re.sub('', value)
0105    return value
0106
0107class HTTPException(Exception):
0108    """
0109    Exception used on pre-Python-2.5, where new-style classes cannot be used as
0110    an exception.
0111    """
0112
0113    def __init__(self, message, wsgi_response):
0114        Exception.__init__(self, message)
0115        self.__dict__['wsgi_response'] = wsgi_response
0116
0117    def __call__(self, environ, start_response):
0118        return self.wsgi_response(environ, start_response)
0119
0120    def exception(self):
0121        return self
0122
0123    exception = property(exception)
0124
0125    if sys.version_info < (2, 5):
0126        def __getattr__(self, attr):
0127            if not attr.startswith('_'):
0128                return getattr(self.wsgi_response, attr)
0129            else:
0130                raise AttributeError(attr)
0131
0132        def __setattr__(self, attr, value):
0133            if attr.startswith('_') or attr in ('args',):
0134                self.__dict__[attr] = value
0135            else:
0136                setattr(self.wsgi_response, attr, value)
0137
0138class WSGIHTTPException(Response, HTTPException):
0139
0140    ## You should set in subclasses:
0141    # code = 200
0142    # title = 'OK'
0143    # explanation = 'why this happens'
0144    # body_template_obj = Template('response template')
0145    code = None
0146    title = None
0147    explanation = ''
0148    body_template_obj = Template('''\
0149${explanation}<br /><br />
0150${detail}
0151${html_comment}
0152''')
0153
0154    plain_template_obj = Template('''\
0155${status}
0156
0157${body}''')
0158
0159    html_template_obj = Template('''\
0160<html>
0161 <head>
0162  <title>${status}</title>
0163 </head>
0164 <body>
0165  <h1>${status}</h1>
0166  ${body}
0167 </body>
0168</html>''')
0169
0170    ## Set this to True for responses that should have no request body
0171    empty_body = False
0172
0173    def __init__(self, detail=None, headers=None, comment=None,
0174                 body_template=None):
0175        Response.__init__(self,
0176                          status='%s %s' % (self.code, self.title),
0177                          content_type='text/html')
0178        Exception.__init__(self, detail)
0179        if headers:
0180            self.headers.update(headers)
0181        self.detail = detail
0182        self.comment = comment
0183        if body_template is not None:
0184            self.body_template = body_template
0185            self.body_template_obj = Template(body_template)
0186        if self.empty_body:
0187            del self.content_type
0188            del self.content_length
0189
0190    def _make_body(self, environ, escape):
0191        args = {
0192            'explanation': escape(self.explanation),
0193            'detail': escape(self.detail or ''),
0194            'comment': escape(self.comment or ''),
0195            }
0196        if self.comment:
0197            args['html_comment'] = '<!-- %s -->' % escape(self.comment)
0198        else:
0199            args['html_comment'] = ''
0200        body_tmpl = self.body_template_obj
0201        if WSGIHTTPException.body_template_obj is not self.body_template_obj:
0202            # Custom template; add headers to args
0203            for k, v in environ.items():
0204                args[k] = escape(v)
0205            for k, v in self.headers.items():
0206                args[k.lower()] = escape(v)
0207        t_obj = self.body_template_obj
0208        return t_obj.substitute(args)
0209
0210    def plain_body(self, environ):
0211        body = self._make_body(environ, no_escape)
0212        body = strip_tags(body)
0213        return self.plain_template_obj.substitute(status=self.status,
0214                                                  title=self.title,
0215                                                  body=body)
0216
0217    def html_body(self, environ):
0218        body = self._make_body(environ, html_escape)
0219        return self.html_template_obj.substitute(status=self.status,
0220                                                 body=body)
0221
0222    def generate_response(self, environ, start_response):
0223        if self.content_length is not None:
0224            del self.content_length
0225        headerlist = list(self.headerlist)
0226        accept = environ.get('HTTP_ACCEPT', '')
0227        if accept and 'html' in accept or '*/*' in accept:
0228            body = self.html_body(environ)
0229            if not self.content_type:
0230                headerlist.append('text/html; charset=utf8')
0231        else:
0232            body = self.plain_body(environ)
0233            if not self.content_type:
0234                headerlist.append('text/plain; charset=utf8')
0235        headerlist.append(('Content-Length', str(len(body))))
0236        start_response(self.status, headerlist)
0237        return [body]
0238
0239    def __call__(self, environ, start_response):
0240        if environ['REQUEST_METHOD'] == 'HEAD':
0241            start_response(self.status, self.headerlist)
0242            return []
0243        if not self.body and not self.empty_body:
0244            return self.generate_response(environ, start_response)
0245        return Response.__call__(self, environ, start_response)
0246
0247    def wsgi_response(self):
0248        return self
0249
0250    wsgi_response = property(wsgi_response)
0251
0252    def exception(self):
0253        if sys.version_info >= (2, 5):
0254            return self
0255        else:
0256            return HTTPException(self.detail, self)
0257
0258    exception = property(exception)
0259
0260class HTTPError(WSGIHTTPException):
0261    """
0262    base class for status codes in the 400's and 500's
0263
0264    This is an exception which indicates that an error has occurred,
0265    and that any work in progress should not be committed.  These are
0266    typically results in the 400's and 500's.
0267    """
0268
0269class HTTPRedirection(WSGIHTTPException):
0270    """
0271    base class for 300's status code (redirections)
0272
0273    This is an abstract base class for 3xx redirection.  It indicates
0274    that further action needs to be taken by the user agent in order
0275    to fulfill the request.  It does not necessarly signal an error
0276    condition.
0277    """
0278
0279class HTTPOk(WSGIHTTPException):
0280    """
0281    Base class for the 200's status code (successful responses)
0282    """
0283    code = 200
0284    title = 'OK'
0285
0286############################################################
0287## 2xx success
0288############################################################
0289
0290class HTTPCreated(HTTPOk):
0291    code = 201
0292    title = 'Created'
0293
0294class HTTPAccepted(HTTPOk):
0295    code = 202
0296    title = 'Accepted'
0297    explanation = 'The request is accepted for processing.'
0298
0299class HTTPNonAuthoritativeInformation(HTTPOk):
0300    code = 203
0301    title = 'Non-Authoritative Information'
0302
0303class HTTPNoContent(HTTPOk):
0304    code = 204
0305    title = 'No Content'
0306    empty_body = True
0307
0308class HTTPResetContent(HTTPOk):
0309    code = 205
0310    title = 'Reset Content'
0311    empty_body = True
0312
0313class HTTPPartialContent(HTTPOk):
0314    code = 206
0315    title = 'Partial Content'
0316
0317## FIXME: add 207 Multi-Status (but it's complicated)
0318
0319############################################################
0320## 3xx redirection
0321############################################################
0322
0323class _HTTPMove(HTTPRedirection):
0324    """
0325    redirections which require a Location field
0326
0327    Since a 'Location' header is a required attribute of 301, 302, 303,
0328    305 and 307 (but not 304), this base class provides the mechanics to
0329    make this easy.
0330
0331    You can provide a location keyword argument to set the location
0332    immediately.  You may also give ``add_slash=True`` if you want to
0333    redirect to the same URL as the request, except with a ``/`` added
0334    to the end.
0335
0336    Relative URLs in the location will be resolved to absolute.
0337    """
0338    explanation = 'The resource has been moved to'
0339    body_template_obj = Template('''\
0340${explanation} <a href="${location}">${location}</a>;
0341you should be redirected automatically.
0342${detail}
0343${html_comment}''')
0344
0345    def __init__(self, detail=None, headers=None, comment=None,
0346                 body_template=None, location=None, add_slash=False):
0347        super(_HTTPMove, self).__init__(
0348            detail=detail, headers=headers, comment=comment,
0349            body_template=body_template)
0350        if location is not None:
0351            self.location = location
0352            if add_slash:
0353                raise TypeError(
0354                    "You can only provide one of the arguments location and add_slash")
0355        self.add_slash = add_slash
0356
0357    def __call__(self, environ, start_response):
0358        req = Request(environ)
0359        if self.add_slash:
0360            url = req.path_url
0361            url += '/'
0362            if req.environ.get('QUERY_STRING'):
0363                url += '?' + req.environ['QUERY_STRING']
0364            self.location = url
0365        self.location = urlparse.urljoin(req.path_url, self.location)
0366        return super(_HTTPMove, self).__call__(
0367            environ, start_response)
0368
0369class HTTPMultipleChoices(_HTTPMove):
0370    code = 300
0371    title = 'Multiple Choices'
0372
0373class HTTPMovedPermanently(_HTTPMove):
0374    code = 301
0375    title = 'Moved Permanently'
0376
0377class HTTPFound(_HTTPMove):
0378    code = 302
0379    title = 'Found'
0380    explanation = 'The resource was found at'
0381
0382# This one is safe after a POST (the redirected location will be
0383# retrieved with GET):
0384class HTTPSeeOther(_HTTPMove):
0385    code = 303
0386    title = 'See Other'
0387
0388class HTTPNotModified(HTTPRedirection):
0389    # FIXME: this should include a date or etag header
0390    code = 304
0391    title = 'Not Modified'
0392    empty_body = True
0393
0394class HTTPUseProxy(_HTTPMove):
0395    # Not a move, but looks a little like one
0396    code = 305
0397    title = 'Use Proxy'
0398    explanation = (
0399        'The resource must be accessed through a proxy located at')
0400
0401class HTTPTemporaryRedirect(_HTTPMove):
0402    code = 307
0403    title = 'Temporary Redirect'
0404
0405############################################################
0406## 4xx client error
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    code = 405
0452    title = 'Method Not Allowed'
0453    # override template since we need an environment variable
0454    body_template_obj = Template('''\
0455The method ${REQUEST_METHOD} is not allowed for this resource. <br /><br />
0456${detail}''')
0457
0458class HTTPNotAcceptable(HTTPClientError):
0459    code = 406
0460    title = 'Not Acceptable'
0461    # override template since we need an environment variable
0462    template = Template('''\
0463The resource could not be generated that was acceptable to your browser
0464(content of type ${HTTP_ACCEPT}. <br /><br />
0465${detail}''')
0466
0467class HTTPProxyAuthenticationRequired(HTTPClientError):
0468    code = 407
0469    title = 'Proxy Authentication Required'
0470    explanation = ('Authentication with a local proxy is needed.')
0471
0472class HTTPRequestTimeout(HTTPClientError):
0473    code = 408
0474    title = 'Request Timeout'
0475    explanation = ('The server has waited too long for the request to '
0476                   'be sent by the client.')
0477
0478class HTTPConflict(HTTPClientError):
0479    code = 409
0480    title = 'Conflict'
0481    explanation = ('There was a conflict when trying to complete '
0482                   'your request.')
0483
0484class HTTPGone(HTTPClientError):
0485    code = 410
0486    title = 'Gone'
0487    explanation = ('This resource is no longer available.  No forwarding '
0488                   'address is given.')
0489
0490class HTTPLengthRequired(HTTPClientError):
0491    code = 411
0492    title = 'Length Required'
0493    explanation = ('Content-Length header required.')
0494
0495class HTTPPreconditionFailed(HTTPClientError):
0496    code = 412
0497    title = 'Precondition Failed'
0498    explanation = ('Request precondition failed.')
0499
0500class HTTPRequestEntityTooLarge(HTTPClientError):
0501    code = 413
0502    title = 'Request Entity Too Large'
0503    explanation = ('The body of your request was too large for this server.')
0504
0505class HTTPRequestURITooLong(HTTPClientError):
0506    code = 414
0507    title = 'Request-URI Too Long'
0508    explanation = ('The request URI was too long for this server.')
0509
0510class HTTPUnsupportedMediaType(HTTPClientError):
0511    code = 415
0512    title = 'Unsupported Media Type'
0513    # override template since we need an environment variable
0514    template_obj = Template('''\
0515The request media type ${CONTENT_TYPE} is not supported by this server.
0516<br /><br />
0517${detail}''')
0518
0519class HTTPRequestRangeNotSatisfiable(HTTPClientError):
0520    code = 416
0521    title = 'Request Range Not Satisfiable'
0522    explanation = ('The Range requested is not available.')
0523
0524class HTTPExpectationFailed(HTTPClientError):
0525    code = 417
0526    title = 'Expectation Failed'
0527    explanation = ('Expectation failed.')
0528
0529class HTTPUnprocessableEntity(HTTPClientError):
0530    ## Note: from WebDAV
0531    code = 422
0532    title = 'Unprocessable Entity'
0533    explanation = 'Unable to process the contained instructions'
0534
0535class HTTPLocked(HTTPClientError):
0536    ## Note: from WebDAV
0537    code = 423
0538    title = 'Locked'
0539    explanation = ('The resource is locked')
0540
0541class HTTPFailedDependency(HTTPClientError):
0542    ## Note: from WebDAV
0543    code = 424
0544    title = 'Failed Dependency'
0545    explanation = ('The method could not be performed because the requested '
0546                   'action dependended on another action and that action failed')
0547
0548############################################################
0549## 5xx Server Error
0550############################################################
0551#  Response status codes beginning with the digit "5" indicate cases in
0552#  which the server is aware that it has erred or is incapable of
0553#  performing the request. Except when responding to a HEAD request, the
0554#  server SHOULD include an entity containing an explanation of the error
0555#  situation, and whether it is a temporary or permanent condition. User
0556#  agents SHOULD display any included entity to the user. These response
0557#  codes are applicable to any request method.
0558
0559class HTTPServerError(HTTPError):
0560    """
0561    base class for the 500's, where the server is in-error
0562
0563    This is an error condition in which the server is presumed to be
0564    in-error.  This is usually unexpected, and thus requires a traceback;
0565    ideally, opening a support ticket for the customer. Unless specialized,
0566    this is a '500 Internal Server Error'
0567    """
0568    code = 500
0569    title = 'Internal Server Error'
0570    explanation = (
0571      'The server has either erred or is incapable of performing\r\n'
0572      'the requested operation.\r\n')
0573
0574class HTTPInternalServerError(HTTPServerError):
0575    pass
0576
0577class HTTPNotImplemented(HTTPServerError):
0578    code = 501
0579    title = 'Not Implemented'
0580    template = Template('''
0581The request method ${REQUEST_METHOD} is not implemented for this server. <br /><br />
0582${detail}''')
0583
0584class HTTPBadGateway(HTTPServerError):
0585    code = 502
0586    title = 'Bad Gateway'
0587    explanation = ('Bad gateway.')
0588
0589class HTTPServiceUnavailable(HTTPServerError):
0590    code = 503
0591    title = 'Service Unavailable'
0592    explanation = ('The server is currently unavailable. '
0593                   'Please try again at a later time.')
0594
0595class HTTPGatewayTimeout(HTTPServerError):
0596    code = 504
0597    title = 'Gateway Timeout'
0598    explanation = ('The gateway has timed out.')
0599
0600class HTTPVersionNotSupported(HTTPServerError):
0601    code = 505
0602    title = 'HTTP Version Not Supported'
0603    explanation = ('The HTTP version is not supported.')
0604
0605class HTTPInsufficientStorage(HTTPServerError):
0606    code = 507
0607    title = 'Insufficient Storage'
0608    explanation = ('There was not enough space to save the resource')
0609
0610class HTTPExceptionMiddleware(object):
0611    """
0612    Middleware that catches exceptions in the sub-application.  This
0613    does not catch exceptions in the app_iter; only during the initial
0614    calling of the application.
0615
0616    This should be put *very close* to applications that might raise
0617    these exceptions.  This should not be applied globally; letting
0618    *expected* exceptions raise through the WSGI stack is dangerous.
0619    """
0620
0621    def __init__(self, application):
0622        self.application = application
0623    def __call__(self, environ, start_response):
0624        try:
0625            return self.application(environ, start_response)
0626        except HTTPException, exc:
0627            parent_exc_info = sys.exc_info()
0628            def repl_start_response(status, headers, exc_info=None):
0629                if exc_info is None:
0630                    exc_info = parent_exc_info
0631                return start_response(status, headers, exc_info)
0632            return exc(environ, repl_start_response)
0633
0634try:
0635    from paste import httpexceptions
0636except ImportError:
0637    # Without Paste we don't need to do this fixup
0638    pass
0639else:
0640    for name in dir(httpexceptions):
0641        obj = globals().get(name)
0642        if (obj and isinstance(obj, type) and issubclass(obj, HTTPException)
0643            and obj is not HTTPException
0644            and obj is not WSGIHTTPException):
0645            obj.__bases__ = obj.__bases__ + (getattr(httpexceptions, name),)
0646    del name, obj, httpexceptions
0647
0648__all__ = ['HTTPExceptionMiddleware', 'status_map']
0649status_map={}
0650for name, value in globals().items():
0651    if (isinstance(value, (type, types.ClassType)) and issubclass(value, HTTPException)
0652        and not name.startswith('_')):
0653        __all__.append(name)
0654        if getattr(value, 'code', None):
0655            status_map[value.code]=value
0656del name, value