1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 # (c) 2005 Ian Bicking, Clark C. Evans and contributors 4 # This module is part of the Python Paste Project and is released under 5 # the MIT License: http://www.opensource.org/licenses/mit-license.php 6 # Some of this code was funded by http://prometheusresearch.com 7 """ 8 HTTP Exception Middleware 9 10 This module processes Python exceptions that relate to HTTP exceptions 11 by defining a set of exceptions, all subclasses of HTTPException, and a 12 request handler (`middleware`) that catches these exceptions and turns 13 them into proper responses. 14 15 This module defines exceptions according to RFC 2068 [1]_ : codes with 16 100-300 are not really errors; 400's are client errors, and 500's are 17 server errors. According to the WSGI specification [2]_ , the application 18 can call ``start_response`` more then once only under two conditions: 19 (a) the response has not yet been sent, or (b) if the second and 20 subsequent invocations of ``start_response`` have a valid ``exc_info`` 21 argument obtained from ``sys.exc_info()``. The WSGI specification then 22 requires the server or gateway to handle the case where content has been 23 sent and then an exception was encountered. 24 25 Exceptions in the 5xx range and those raised after ``start_response`` 26 has been called are treated as serious errors and the ``exc_info`` is 27 filled-in with information needed for a lower level module to generate a 28 stack trace and log information. 29 30 Exception 31 HTTPException 32 HTTPRedirection 33 * 300 - HTTPMultipleChoices 34 * 301 - HTTPMovedPermanently 35 * 302 - HTTPFound 36 * 303 - HTTPSeeOther 37 * 304 - HTTPNotModified 38 * 305 - HTTPUseProxy 39 * 306 - Unused (not implemented, obviously) 40 * 307 - HTTPTemporaryRedirect 41 HTTPError 42 HTTPClientError 43 * 400 - HTTPBadRequest 44 * 401 - HTTPUnauthorized 45 * 402 - HTTPPaymentRequired 46 * 403 - HTTPForbidden 47 * 404 - HTTPNotFound 48 * 405 - HTTPMethodNotAllowed 49 * 406 - HTTPNotAcceptable 50 * 407 - HTTPProxyAuthenticationRequired 51 * 408 - HTTPRequestTimeout 52 * 409 - HTTPConfict 53 * 410 - HTTPGone 54 * 411 - HTTPLengthRequired 55 * 412 - HTTPPreconditionFailed 56 * 413 - HTTPRequestEntityTooLarge 57 * 414 - HTTPRequestURITooLong 58 * 415 - HTTPUnsupportedMediaType 59 * 416 - HTTPRequestRangeNotSatisfiable 60 * 417 - HTTPExpectationFailed 61 * 429 - HTTPTooManyRequests 62 HTTPServerError 63 * 500 - HTTPInternalServerError 64 * 501 - HTTPNotImplemented 65 * 502 - HTTPBadGateway 66 * 503 - HTTPServiceUnavailable 67 * 504 - HTTPGatewayTimeout 68 * 505 - HTTPVersionNotSupported 69 70 References: 71 72 .. [1] http://www.python.org/peps/pep-0333.html#error-handling 73 .. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5 74 75 """ 76 77 import six 78 from paste.wsgilib import catch_errors_app 79 from paste.response import has_header, header_value, replace_header 80 from paste.request import resolve_relative_url 81 from paste.util.quoting import strip_html, html_quote, no_quote, comment_quote 82 83 SERVER_NAME = 'WSGI Server' 84 TEMPLATE = """\ 85 <html>\r 86 <head><title>%(title)s</title></head>\r 87 <body>\r 88 <h1>%(title)s</h1>\r 89 <p>%(body)s</p>\r 90 <hr noshade>\r 91 <div align="right">%(server)s</div>\r 92 </body>\r 93 </html>\r 94 """ 95 96 class HTTPException(Exception): 97 """ 98 the HTTP exception base class 99 100 This encapsulates an HTTP response that interrupts normal application 101 flow; but one which is not necessarly an error condition. For 102 example, codes in the 300's are exceptions in that they interrupt 103 normal processing; however, they are not considered errors. 104 105 This class is complicated by 4 factors: 106 107 1. The content given to the exception may either be plain-text or 108 as html-text. 109 110 2. The template may want to have string-substitutions taken from 111 the current ``environ`` or values from incoming headers. This 112 is especially troublesome due to case sensitivity. 113 114 3. The final output may either be text/plain or text/html 115 mime-type as requested by the client application. 116 117 4. Each exception has a default explanation, but those who 118 raise exceptions may want to provide additional detail. 119 120 Attributes: 121 122 ``code`` 123 the HTTP status code for the exception 124 125 ``title`` 126 remainder of the status line (stuff after the code) 127 128 ``explanation`` 129 a plain-text explanation of the error message that is 130 not subject to environment or header substitutions; 131 it is accessible in the template via %(explanation)s 132 133 ``detail`` 134 a plain-text message customization that is not subject 135 to environment or header substitutions; accessible in 136 the template via %(detail)s 137 138 ``template`` 139 a content fragment (in HTML) used for environment and 140 header substitution; the default template includes both 141 the explanation and further detail provided in the 142 message 143 144 ``required_headers`` 145 a sequence of headers which are required for proper 146 construction of the exception 147 148 Parameters: 149 150 ``detail`` 151 a plain-text override of the default ``detail`` 152 153 ``headers`` 154 a list of (k,v) header pairs 155 156 ``comment`` 157 a plain-text additional information which is 158 usually stripped/hidden for end-users 159 160 To override the template (which is HTML content) or the plain-text 161 explanation, one must subclass the given exception; or customize it 162 after it has been created. This particular breakdown of a message 163 into explanation, detail and template allows both the creation of 164 plain-text and html messages for various clients as well as 165 error-free substitution of environment variables and headers. 166 """ 167 168 code = None 169 title = None 170 explanation = '' 171 detail = '' 172 comment = '' 173 template = "%(explanation)s\r\n<br/>%(detail)s\r\n<!-- %(comment)s -->" 174 required_headers = () 175 176 def __init__(self, detail=None, headers=None, comment=None): 177 assert self.code, "Do not directly instantiate abstract exceptions." 178 assert isinstance(headers, (type(None), list)), ( 179 "headers must be None or a list: %r" 180 % headers) 181 assert isinstance(detail, (type(None), six.binary_type, six.text_type)), ( 182 "detail must be None or a string: %r" % detail) 183 assert isinstance(comment, (type(None), six.binary_type, six.text_type)), ( 184 "comment must be None or a string: %r" % comment) 185 self.headers = headers or tuple() 186 for req in self.required_headers: 187 assert headers and has_header(headers, req), ( 188 "Exception %s must be passed the header %r " 189 "(got headers: %r)" 190 % (self.__class__.__name__, req, headers)) 191 if detail is not None: 192 self.detail = detail 193 if comment is not None: 194 self.comment = comment 195 Exception.__init__(self,"%s %s\n%s\n%s\n" % ( 196 self.code, self.title, self.explanation, self.detail)) 197 198 def make_body(self, environ, template, escfunc, comment_escfunc=None): 199 comment_escfunc = comment_escfunc or escfunc 200 args = {'explanation': escfunc(self.explanation), 201 'detail': escfunc(self.detail), 202 'comment': comment_escfunc(self.comment)} 203 if HTTPException.template != self.template: 204 for (k, v) in environ.items(): 205 args[k] = escfunc(v) 206 if self.headers: 207 for (k, v) in self.headers: 208 args[k.lower()] = escfunc(v) 209 if six.PY2: 210 for key, value in args.items(): 211 if isinstance(value, six.text_type): 212 args[key] = value.encode('utf8', 'xmlcharrefreplace') 213 return template % args 214 215 def plain(self, environ): 216 """ text/plain representation of the exception """ 217 body = self.make_body(environ, strip_html(self.template), no_quote, comment_quote) 218 return ('%s %s\r\n%s\r\n' % (self.code, self.title, body)) 219 220 def html(self, environ): 221 """ text/html representation of the exception """ 222 body = self.make_body(environ, self.template, html_quote, comment_quote) 223 return TEMPLATE % { 224 'title': self.title, 225 'code': self.code, 226 'server': SERVER_NAME, 227 'body': body } 228 229 def prepare_content(self, environ): 230 if self.headers: 231 headers = list(self.headers) 232 else: 233 headers = [] 234 if 'html' in environ.get('HTTP_ACCEPT','') or \ 235 '*/*' in environ.get('HTTP_ACCEPT',''): 236 replace_header(headers, 'content-type', 'text/html') 237 content = self.html(environ) 238 else: 239 replace_header(headers, 'content-type', 'text/plain') 240 content = self.plain(environ) 241 if isinstance(content, six.text_type): 242 content = content.encode('utf8') 243 cur_content_type = ( 244 header_value(headers, 'content-type') 245 or 'text/html') 246 replace_header( 247 headers, 'content-type', 248 cur_content_type + '; charset=utf8') 249 return headers, content 250 251 def response(self, environ): 252 from paste.wsgiwrappers import WSGIResponse 253 headers, content = self.prepare_content(environ) 254 resp = WSGIResponse(code=self.code, content=content) 255 resp.headers = resp.headers.fromlist(headers) 256 return resp 257 258 def wsgi_application(self, environ, start_response, exc_info=None): 259 """ 260 This exception as a WSGI application 261 """ 262 headers, content = self.prepare_content(environ) 263 start_response('%s %s' % (self.code, self.title), 264 headers, 265 exc_info) 266 return [content] 267 268 __call__ = wsgi_application 269 270 def __repr__(self): 271 return '<%s %s; code=%s>' % (self.__class__.__name__, 272 self.title, self.code) 273 274 class HTTPError(HTTPException): 275 """ 276 base class for status codes in the 400's and 500's 277 278 This is an exception which indicates that an error has occurred, 279 and that any work in progress should not be committed. These are 280 typically results in the 400's and 500's. 281 """ 282 283 # 284 # 3xx Redirection 285 # 286 # This class of status code indicates that further action needs to be 287 # taken by the user agent in order to fulfill the request. The action 288 # required MAY be carried out by the user agent without interaction with 289 # the user if and only if the method used in the second request is GET or 290 # HEAD. A client SHOULD detect infinite redirection loops, since such 291 # loops generate network traffic for each redirection. 292 # 293 294 class HTTPRedirection(HTTPException): 295 """ 296 base class for 300's status code (redirections) 297 298 This is an abstract base class for 3xx redirection. It indicates 299 that further action needs to be taken by the user agent in order 300 to fulfill the request. It does not necessarly signal an error 301 condition. 302 """ 303 304 class _HTTPMove(HTTPRedirection): 305 """ 306 redirections which require a Location field 307 308 Since a 'Location' header is a required attribute of 301, 302, 303, 309 305 and 307 (but not 304), this base class provides the mechanics to 310 make this easy. While this has the same parameters as HTTPException, 311 if a location is not provided in the headers; it is assumed that the 312 detail _is_ the location (this for backward compatibility, otherwise 313 we'd add a new attribute). 314 """ 315 required_headers = ('location',) 316 explanation = 'The resource has been moved to' 317 template = ( 318 '%(explanation)s <a href="%(location)s">%(location)s</a>;\r\n' 319 'you should be redirected automatically.\r\n' 320 '%(detail)s\r\n<!-- %(comment)s -->') 321 322 def __init__(self, detail=None, headers=None, comment=None): 323 assert isinstance(headers, (type(None), list)) 324 headers = headers or [] 325 location = header_value(headers,'location') 326 if not location: 327 location = detail 328 detail = '' 329 headers.append(('location', location)) 330 assert location, ("HTTPRedirection specified neither a " 331 "location in the headers nor did it " 332 "provide a detail argument.") 333 HTTPRedirection.__init__(self, location, headers, comment) 334 if detail is not None: 335 self.detail = detail 336 337 def relative_redirect(cls, dest_uri, environ, detail=None, headers=None, comment=None): 338 """ 339 Create a redirect object with the dest_uri, which may be relative, 340 considering it relative to the uri implied by the given environ. 341 """ 342 location = resolve_relative_url(dest_uri, environ) 343 headers = headers or [] 344 headers.append(('Location', location)) 345 return cls(detail=detail, headers=headers, comment=comment) 346 347 relative_redirect = classmethod(relative_redirect) 348 349 def location(self): 350 for name, value in self.headers: 351 if name.lower() == 'location': 352 return value 353 else: 354 raise KeyError("No location set for %s" % self) 355 356 class HTTPMultipleChoices(_HTTPMove): 357 code = 300 358 title = 'Multiple Choices' 359 360 class HTTPMovedPermanently(_HTTPMove): 361 code = 301 362 title = 'Moved Permanently' 363 364 class HTTPFound(_HTTPMove): 365 code = 302 366 title = 'Found' 367 explanation = 'The resource was found at' 368 369 # This one is safe after a POST (the redirected location will be 370 # retrieved with GET): 371 class HTTPSeeOther(_HTTPMove): 372 code = 303 373 title = 'See Other' 374 375 class HTTPNotModified(HTTPRedirection): 376 # @@: but not always (HTTP section 14.18.1)...? 377 # @@: Removed 'date' requirement, as its not required for an ETag 378 # @@: FIXME: This should require either an ETag or a date header 379 code = 304 380 title = 'Not Modified' 381 message = '' 382 # @@: should include date header, optionally other headers 383 # @@: should not return a content body 384 def plain(self, environ): 385 return '' 386 def html(self, environ): 387 """ text/html representation of the exception """ 388 return '' 389 390 class HTTPUseProxy(_HTTPMove): 391 # @@: OK, not a move, but looks a little like one 392 code = 305 393 title = 'Use Proxy' 394 explanation = ( 395 'The resource must be accessed through a proxy ' 396 'located at') 397 398 class HTTPTemporaryRedirect(_HTTPMove): 399 code = 307 400 title = 'Temporary Redirect' 401 402 # 403 # 4xx Client Error 404 # 405 # The 4xx class of status code is intended for cases in which the client 406 # seems to have erred. Except when responding to a HEAD request, the 407 # server SHOULD include an entity containing an explanation of the error 408 # situation, and whether it is a temporary or permanent condition. These 409 # status codes are applicable to any request method. User agents SHOULD 410 # display any included entity to the user. 411 # 412 413 class HTTPClientError(HTTPError): 414 """ 415 base class for the 400's, where the client is in-error 416 417 This is an error condition in which the client is presumed to be 418 in-error. This is an expected problem, and thus is not considered 419 a bug. A server-side traceback is not warranted. Unless specialized, 420 this is a '400 Bad Request' 421 """ 422 code = 400 423 title = 'Bad Request' 424 explanation = ('The server could not comply with the request since\r\n' 425 'it is either malformed or otherwise incorrect.\r\n') 426 427 class HTTPBadRequest(HTTPClientError): 428 pass 429 430 class HTTPUnauthorized(HTTPClientError): 431 code = 401 432 title = 'Unauthorized' 433 explanation = ( 434 'This server could not verify that you are authorized to\r\n' 435 'access the document you requested. Either you supplied the\r\n' 436 'wrong credentials (e.g., bad password), or your browser\r\n' 437 'does not understand how to supply the credentials required.\r\n') 438 439 class HTTPPaymentRequired(HTTPClientError): 440 code = 402 441 title = 'Payment Required' 442 explanation = ('Access was denied for financial reasons.') 443 444 class HTTPForbidden(HTTPClientError): 445 code = 403 446 title = 'Forbidden' 447 explanation = ('Access was denied to this resource.') 448 449 class HTTPNotFound(HTTPClientError): 450 code = 404 451 title = 'Not Found' 452 explanation = ('The resource could not be found.') 453 454 class HTTPMethodNotAllowed(HTTPClientError): 455 required_headers = ('allow',) 456 code = 405 457 title = 'Method Not Allowed' 458 # override template since we need an environment variable 459 template = ('The method %(REQUEST_METHOD)s is not allowed for ' 460 'this resource.\r\n%(detail)s') 461 462 class HTTPNotAcceptable(HTTPClientError): 463 code = 406 464 title = 'Not Acceptable' 465 # override template since we need an environment variable 466 template = ('The resource could not be generated that was ' 467 'acceptable to your browser (content\r\nof type ' 468 '%(HTTP_ACCEPT)s).\r\n%(detail)s') 469 470 class HTTPProxyAuthenticationRequired(HTTPClientError): 471 code = 407 472 title = 'Proxy Authentication Required' 473 explanation = ('Authentication /w a local proxy is needed.') 474 475 class HTTPRequestTimeout(HTTPClientError): 476 code = 408 477 title = 'Request Timeout' 478 explanation = ('The server has waited too long for the request to ' 479 'be sent by the client.') 480 481 class HTTPConflict(HTTPClientError): 482 code = 409 483 title = 'Conflict' 484 explanation = ('There was a conflict when trying to complete ' 485 'your request.') 486 487 class HTTPGone(HTTPClientError): 488 code = 410 489 title = 'Gone' 490 explanation = ('This resource is no longer available. No forwarding ' 491 'address is given.') 492 493 class HTTPLengthRequired(HTTPClientError): 494 code = 411 495 title = 'Length Required' 496 explanation = ('Content-Length header required.') 497 498 class HTTPPreconditionFailed(HTTPClientError): 499 code = 412 500 title = 'Precondition Failed' 501 explanation = ('Request precondition failed.') 502 503 class HTTPRequestEntityTooLarge(HTTPClientError): 504 code = 413 505 title = 'Request Entity Too Large' 506 explanation = ('The body of your request was too large for this server.') 507 508 class HTTPRequestURITooLong(HTTPClientError): 509 code = 414 510 title = 'Request-URI Too Long' 511 explanation = ('The request URI was too long for this server.') 512 513 class HTTPUnsupportedMediaType(HTTPClientError): 514 code = 415 515 title = 'Unsupported Media Type' 516 # override template since we need an environment variable 517 template = ('The request media type %(CONTENT_TYPE)s is not ' 518 'supported by this server.\r\n%(detail)s') 519 520 class HTTPRequestRangeNotSatisfiable(HTTPClientError): 521 code = 416 522 title = 'Request Range Not Satisfiable' 523 explanation = ('The Range requested is not available.') 524 525 class HTTPExpectationFailed(HTTPClientError): 526 code = 417 527 title = 'Expectation Failed' 528 explanation = ('Expectation failed.') 529 530 class HTTPTooManyRequests(HTTPClientError): 531 code = 429 532 title = 'Too Many Requests' 533 explanation = ('The client has sent too many requests to the server.') 534 535 # 536 # 5xx Server Error 537 # 538 # Response status codes beginning with the digit "5" indicate cases in 539 # which the server is aware that it has erred or is incapable of 540 # performing the request. Except when responding to a HEAD request, the 541 # server SHOULD include an entity containing an explanation of the error 542 # situation, and whether it is a temporary or permanent condition. User 543 # agents SHOULD display any included entity to the user. These response 544 # codes are applicable to any request method. 545 # 546 547 class HTTPServerError(HTTPError): 548 """ 549 base class for the 500's, where the server is in-error 550 551 This is an error condition in which the server is presumed to be 552 in-error. This is usually unexpected, and thus requires a traceback; 553 ideally, opening a support ticket for the customer. Unless specialized, 554 this is a '500 Internal Server Error' 555 """ 556 code = 500 557 title = 'Internal Server Error' 558 explanation = ( 559 'The server has either erred or is incapable of performing\r\n' 560 'the requested operation.\r\n') 561 562 class HTTPInternalServerError(HTTPServerError): 563 pass 564 565 class HTTPNotImplemented(HTTPServerError): 566 code = 501 567 title = 'Not Implemented' 568 # override template since we need an environment variable 569 template = ('The request method %(REQUEST_METHOD)s is not implemented ' 570 'for this server.\r\n%(detail)s') 571 572 class HTTPBadGateway(HTTPServerError): 573 code = 502 574 title = 'Bad Gateway' 575 explanation = ('Bad gateway.') 576 577 class HTTPServiceUnavailable(HTTPServerError): 578 code = 503 579 title = 'Service Unavailable' 580 explanation = ('The server is currently unavailable. ' 581 'Please try again at a later time.') 582 583 class HTTPGatewayTimeout(HTTPServerError): 584 code = 504 585 title = 'Gateway Timeout' 586 explanation = ('The gateway has timed out.') 587 588 class HTTPVersionNotSupported(HTTPServerError): 589 code = 505 590 title = 'HTTP Version Not Supported' 591 explanation = ('The HTTP version is not supported.') 592 593 # abstract HTTP related exceptions 594 __all__ = ['HTTPException', 'HTTPRedirection', 'HTTPError' ] 595 596 _exceptions = {} 597 for name, value in six.iteritems(dict(globals())): 598 if (isinstance(value, (type, six.class_types)) and 599 issubclass(value, HTTPException) and 600 value.code): 601 _exceptions[value.code] = value 602 __all__.append(name) 603 604 def get_exception(code): 605 return _exceptions[code] 606 607 ############################################################ 608 ## Middleware implementation: 609 ############################################################ 610 611 class HTTPExceptionHandler(object): 612 """ 613 catches exceptions and turns them into proper HTTP responses 614 615 This middleware catches any exceptions (which are subclasses of 616 ``HTTPException``) and turns them into proper HTTP responses. 617 Note if the headers have already been sent, the stack trace is 618 always maintained as this indicates a programming error. 619 620 Note that you must raise the exception before returning the 621 app_iter, and you cannot use this with generator apps that don't 622 raise an exception until after their app_iter is iterated over. 623 """ 624 625 def __init__(self, application, warning_level=None): 626 assert not warning_level or ( warning_level > 99 and 627 warning_level < 600) 628 if warning_level is not None: 629 import warnings 630 warnings.warn('The warning_level parameter is not used or supported', 631 DeprecationWarning, 2) 632 self.warning_level = warning_level or 500 633 self.application = application 634 635 def __call__(self, environ, start_response): 636 environ['paste.httpexceptions'] = self 637 environ.setdefault('paste.expected_exceptions', 638 []).append(HTTPException) 639 try: 640 return self.application(environ, start_response) 641 except HTTPException as exc: 642 return exc(environ, start_response) 643 644 def middleware(*args, **kw): 645 import warnings 646 # deprecated 13 dec 2005 647 warnings.warn('httpexceptions.middleware is deprecated; use ' 648 'make_middleware or HTTPExceptionHandler instead', 649 DeprecationWarning, 2) 650 return make_middleware(*args, **kw) 651 652 def make_middleware(app, global_conf=None, warning_level=None): 653 """ 654 ``httpexceptions`` middleware; this catches any 655 ``paste.httpexceptions.HTTPException`` exceptions (exceptions like 656 ``HTTPNotFound``, ``HTTPMovedPermanently``, etc) and turns them 657 into proper HTTP responses. 658 659 ``warning_level`` can be an integer corresponding to an HTTP code. 660 Any code over that value will be passed 'up' the chain, potentially 661 reported on by another piece of middleware. 662 """ 663 if warning_level: 664 warning_level = int(warning_level) 665 return HTTPExceptionHandler(app, warning_level=warning_level) 666 667 __all__.extend(['HTTPExceptionHandler', 'get_exception']) 668