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
0141
0142
0143
0144
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
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
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
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
0318
0319
0320
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
0383
0384class HTTPSeeOther(_HTTPMove):
0385 code = 303
0386 title = 'See Other'
0387
0388class HTTPNotModified(HTTPRedirection):
0389
0390 code = 304
0391 title = 'Not Modified'
0392 empty_body = True
0393
0394class HTTPUseProxy(_HTTPMove):
0395
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
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
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
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
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
0531 code = 422
0532 title = 'Unprocessable Entity'
0533 explanation = 'Unable to process the contained instructions'
0534
0535class HTTPLocked(HTTPClientError):
0536
0537 code = 423
0538 title = 'Locked'
0539 explanation = ('The resource is locked')
0540
0541class HTTPFailedDependency(HTTPClientError):
0542
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
0550
0551
0552
0553
0554
0555
0556
0557
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
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