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