Package cherrypy :: Module _cperror
[hide private]
[frames] | no frames]

Source Code for Module cherrypy._cperror

  1  """Exception classes for CherryPy. 
  2   
  3  CherryPy provides (and uses) exceptions for declaring that the HTTP response 
  4  should be a status other than the default "200 OK". You can ``raise`` them like 
  5  normal Python exceptions. You can also call them and they will raise 
  6  themselves; this means you can set an 
  7  :class:`HTTPError<cherrypy._cperror.HTTPError>` 
  8  or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the 
  9  :attr:`request.handler<cherrypy._cprequest.Request.handler>`. 
 10   
 11  .. _redirectingpost: 
 12   
 13  Redirecting POST 
 14  ================ 
 15   
 16  When you GET a resource and are redirected by the server to another Location, 
 17  there's generally no problem since GET is both a "safe method" (there should 
 18  be no side-effects) and an "idempotent method" (multiple calls are no different 
 19  than a single call). 
 20   
 21  POST, however, is neither safe nor idempotent--if you 
 22  charge a credit card, you don't want to be charged twice by a redirect! 
 23   
 24  For this reason, *none* of the 3xx responses permit a user-agent (browser) to 
 25  resubmit a POST on redirection without first confirming the action with the 
 26  user: 
 27   
 28  =====    =================================    =========== 
 29  300      Multiple Choices                     Confirm with the user 
 30  301      Moved Permanently                    Confirm with the user 
 31  302      Found (Object moved temporarily)     Confirm with the user 
 32  303      See Other                            GET the new URI--no confirmation 
 33  304      Not modified                         (for conditional GET only--POST should not raise this error) 
 34  305      Use Proxy                            Confirm with the user 
 35  307      Temporary Redirect                   Confirm with the user 
 36  =====    =================================    =========== 
 37   
 38  However, browsers have historically implemented these restrictions poorly; 
 39  in particular, many browsers do not force the user to confirm 301, 302 
 40  or 307 when redirecting POST. For this reason, CherryPy defaults to 303, 
 41  which most user-agents appear to have implemented correctly. Therefore, if 
 42  you raise HTTPRedirect for a POST request, the user-agent will most likely 
 43  attempt to GET the new URI (without asking for confirmation from the user). 
 44  We realize this is confusing for developers, but it's the safest thing we 
 45  could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` 
 46  or any other 3xx status if you know what you're doing, but given the 
 47  environment, we couldn't let any of those be the default. 
 48   
 49  Custom Error Handling 
 50  ===================== 
 51   
 52  .. image:: /refman/cperrors.gif 
 53   
 54  Anticipated HTTP responses 
 55  -------------------------- 
 56   
 57  The 'error_page' config namespace can be used to provide custom HTML output for 
 58  expected responses (like 404 Not Found). Supply a filename from which the 
 59  output will be read. The contents will be interpolated with the values 
 60  %(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python 
 61  `string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_. 
 62   
 63  :: 
 64   
 65      _cp_config = { 
 66          'error_page.404': os.path.join(localDir, "static/index.html") 
 67      } 
 68   
 69   
 70  Beginning in version 3.1, you may also provide a function or other callable as 
 71  an error_page entry. It will be passed the same status, message, traceback and 
 72  version arguments that are interpolated into templates:: 
 73   
 74      def error_page_402(status, message, traceback, version): 
 75          return "Error %s - Well, I'm very sorry but you haven't paid!" % status 
 76      cherrypy.config.update({'error_page.402': error_page_402}) 
 77   
 78  Also in 3.1, in addition to the numbered error codes, you may also supply 
 79  "error_page.default" to handle all codes which do not have their own error_page 
 80  entry. 
 81   
 82   
 83   
 84  Unanticipated errors 
 85  -------------------- 
 86   
 87  CherryPy also has a generic error handling mechanism: whenever an unanticipated 
 88  error occurs in your code, it will call 
 89  :func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to 
 90  set the response status, headers, and body. By default, this is the same 
 91  output as 
 92  :class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide 
 93  some other behavior, you generally replace "request.error_response". 
 94   
 95  Here is some sample code that shows how to display a custom error message and 
 96  send an e-mail containing the error:: 
 97   
 98      from cherrypy import _cperror 
 99   
100      def handle_error(): 
101          cherrypy.response.status = 500 
102          cherrypy.response.body = [ 
103              "<html><body>Sorry, an error occured</body></html>" 
104          ] 
105          sendMail('error@domain.com', 
106                   'Error in your web app', 
107                   _cperror.format_exc()) 
108   
109      class Root: 
110          _cp_config = {'request.error_response': handle_error} 
111   
112   
113  Note that you have to explicitly set 
114  :attr:`response.body <cherrypy._cprequest.Response.body>` 
115  and not simply return an error message as a result. 
116  """ 
117   
118  from cgi import escape as _escape 
119  from sys import exc_info as _exc_info 
120  from traceback import format_exception as _format_exception 
121  from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob 
122  from cherrypy._cpcompat import tonative, urljoin as _urljoin 
123  from cherrypy.lib import httputil as _httputil 
124   
125   
126 -class CherryPyException(Exception):
127 128 """A base class for CherryPy exceptions.""" 129 pass
130 131
132 -class TimeoutError(CherryPyException):
133 134 """Exception raised when Response.timed_out is detected.""" 135 pass
136 137
138 -class InternalRedirect(CherryPyException):
139 140 """Exception raised to switch to the handler for a different URL. 141 142 This exception will redirect processing to another path within the site 143 (without informing the client). Provide the new path as an argument when 144 raising the exception. Provide any params in the querystring for the new 145 URL. 146 """ 147
148 - def __init__(self, path, query_string=""):
149 import cherrypy 150 self.request = cherrypy.serving.request 151 152 self.query_string = query_string 153 if "?" in path: 154 # Separate any params included in the path 155 path, self.query_string = path.split("?", 1) 156 157 # Note that urljoin will "do the right thing" whether url is: 158 # 1. a URL relative to root (e.g. "/dummy") 159 # 2. a URL relative to the current path 160 # Note that any query string will be discarded. 161 path = _urljoin(self.request.path_info, path) 162 163 # Set a 'path' member attribute so that code which traps this 164 # error can have access to it. 165 self.path = path 166 167 CherryPyException.__init__(self, path, self.query_string)
168 169
170 -class HTTPRedirect(CherryPyException):
171 172 """Exception raised when the request should be redirected. 173 174 This exception will force a HTTP redirect to the URL or URL's you give it. 175 The new URL must be passed as the first argument to the Exception, 176 e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. 177 If a URL is absolute, it will be used as-is. If it is relative, it is 178 assumed to be relative to the current cherrypy.request.path_info. 179 180 If one of the provided URL is a unicode object, it will be encoded 181 using the default encoding or the one passed in parameter. 182 183 There are multiple types of redirect, from which you can select via the 184 ``status`` argument. If you do not provide a ``status`` arg, it defaults to 185 303 (or 302 if responding with HTTP/1.0). 186 187 Examples:: 188 189 raise cherrypy.HTTPRedirect("") 190 raise cherrypy.HTTPRedirect("/abs/path", 307) 191 raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) 192 193 See :ref:`redirectingpost` for additional caveats. 194 """ 195 196 status = None 197 """The integer HTTP status code to emit.""" 198 199 urls = None 200 """The list of URL's to emit.""" 201 202 encoding = 'utf-8' 203 """The encoding when passed urls are not native strings""" 204
205 - def __init__(self, urls, status=None, encoding=None):
206 import cherrypy 207 request = cherrypy.serving.request 208 209 if isinstance(urls, basestring): 210 urls = [urls] 211 212 abs_urls = [] 213 for url in urls: 214 url = tonative(url, encoding or self.encoding) 215 216 # Note that urljoin will "do the right thing" whether url is: 217 # 1. a complete URL with host (e.g. "http://www.example.com/test") 218 # 2. a URL relative to root (e.g. "/dummy") 219 # 3. a URL relative to the current path 220 # Note that any query string in cherrypy.request is discarded. 221 url = _urljoin(cherrypy.url(), url) 222 abs_urls.append(url) 223 self.urls = abs_urls 224 225 # RFC 2616 indicates a 301 response code fits our goal; however, 226 # browser support for 301 is quite messy. Do 302/303 instead. See 227 # http://www.alanflavell.org.uk/www/post-redirect.html 228 if status is None: 229 if request.protocol >= (1, 1): 230 status = 303 231 else: 232 status = 302 233 else: 234 status = int(status) 235 if status < 300 or status > 399: 236 raise ValueError("status must be between 300 and 399.") 237 238 self.status = status 239 CherryPyException.__init__(self, abs_urls, status)
240
241 - def set_response(self):
242 """Modify cherrypy.response status, headers, and body to represent 243 self. 244 245 CherryPy uses this internally, but you can also use it to create an 246 HTTPRedirect object and set its output without *raising* the exception. 247 """ 248 import cherrypy 249 response = cherrypy.serving.response 250 response.status = status = self.status 251 252 if status in (300, 301, 302, 303, 307): 253 response.headers['Content-Type'] = "text/html;charset=utf-8" 254 # "The ... URI SHOULD be given by the Location field 255 # in the response." 256 response.headers['Location'] = self.urls[0] 257 258 # "Unless the request method was HEAD, the entity of the response 259 # SHOULD contain a short hypertext note with a hyperlink to the 260 # new URI(s)." 261 msg = { 262 300: "This resource can be found at ", 263 301: "This resource has permanently moved to ", 264 302: "This resource resides temporarily at ", 265 303: "This resource can be found at ", 266 307: "This resource has moved temporarily to ", 267 }[status] 268 msg += '<a href=%s>%s</a>.' 269 from xml.sax import saxutils 270 msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls] 271 response.body = ntob("<br />\n".join(msgs), 'utf-8') 272 # Previous code may have set C-L, so we have to reset it 273 # (allow finalize to set it). 274 response.headers.pop('Content-Length', None) 275 elif status == 304: 276 # Not Modified. 277 # "The response MUST include the following header fields: 278 # Date, unless its omission is required by section 14.18.1" 279 # The "Date" header should have been set in Response.__init__ 280 281 # "...the response SHOULD NOT include other entity-headers." 282 for key in ('Allow', 'Content-Encoding', 'Content-Language', 283 'Content-Length', 'Content-Location', 'Content-MD5', 284 'Content-Range', 'Content-Type', 'Expires', 285 'Last-Modified'): 286 if key in response.headers: 287 del response.headers[key] 288 289 # "The 304 response MUST NOT contain a message-body." 290 response.body = None 291 # Previous code may have set C-L, so we have to reset it. 292 response.headers.pop('Content-Length', None) 293 elif status == 305: 294 # Use Proxy. 295 # self.urls[0] should be the URI of the proxy. 296 response.headers['Location'] = self.urls[0] 297 response.body = None 298 # Previous code may have set C-L, so we have to reset it. 299 response.headers.pop('Content-Length', None) 300 else: 301 raise ValueError("The %s status code is unknown." % status)
302
303 - def __call__(self):
304 """Use this exception as a request.handler (raise self).""" 305 raise self
306 307
308 -def clean_headers(status):
309 """Remove any headers which should not apply to an error response.""" 310 import cherrypy 311 312 response = cherrypy.serving.response 313 314 # Remove headers which applied to the original content, 315 # but do not apply to the error page. 316 respheaders = response.headers 317 for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After", 318 "Vary", "Content-Encoding", "Content-Length", "Expires", 319 "Content-Location", "Content-MD5", "Last-Modified"]: 320 if key in respheaders: 321 del respheaders[key] 322 323 if status != 416: 324 # A server sending a response with status code 416 (Requested 325 # range not satisfiable) SHOULD include a Content-Range field 326 # with a byte-range-resp-spec of "*". The instance-length 327 # specifies the current length of the selected resource. 328 # A response with status code 206 (Partial Content) MUST NOT 329 # include a Content-Range field with a byte-range- resp-spec of "*". 330 if "Content-Range" in respheaders: 331 del respheaders["Content-Range"]
332 333
334 -class HTTPError(CherryPyException):
335 336 """Exception used to return an HTTP error code (4xx-5xx) to the client. 337 338 This exception can be used to automatically send a response using a 339 http status code, with an appropriate error page. It takes an optional 340 ``status`` argument (which must be between 400 and 599); it defaults to 500 341 ("Internal Server Error"). It also takes an optional ``message`` argument, 342 which will be returned in the response body. See 343 `RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_ 344 for a complete list of available error codes and when to use them. 345 346 Examples:: 347 348 raise cherrypy.HTTPError(403) 349 raise cherrypy.HTTPError( 350 "403 Forbidden", "You are not allowed to access this resource.") 351 """ 352 353 status = None 354 """The HTTP status code. May be of type int or str (with a Reason-Phrase). 355 """ 356 357 code = None 358 """The integer HTTP status code.""" 359 360 reason = None 361 """The HTTP Reason-Phrase string.""" 362
363 - def __init__(self, status=500, message=None):
364 self.status = status 365 try: 366 self.code, self.reason, defaultmsg = _httputil.valid_status(status) 367 except ValueError: 368 raise self.__class__(500, _exc_info()[1].args[0]) 369 370 if self.code < 400 or self.code > 599: 371 raise ValueError("status must be between 400 and 599.") 372 373 # See http://www.python.org/dev/peps/pep-0352/ 374 # self.message = message 375 self._message = message or defaultmsg 376 CherryPyException.__init__(self, status, message)
377
378 - def set_response(self):
379 """Modify cherrypy.response status, headers, and body to represent 380 self. 381 382 CherryPy uses this internally, but you can also use it to create an 383 HTTPError object and set its output without *raising* the exception. 384 """ 385 import cherrypy 386 387 response = cherrypy.serving.response 388 389 clean_headers(self.code) 390 391 # In all cases, finalize will be called after this method, 392 # so don't bother cleaning up response values here. 393 response.status = self.status 394 tb = None 395 if cherrypy.serving.request.show_tracebacks: 396 tb = format_exc() 397 398 response.headers.pop('Content-Length', None) 399 400 content = self.get_error_page(self.status, traceback=tb, 401 message=self._message) 402 response.body = content 403 404 _be_ie_unfriendly(self.code)
405
406 - def get_error_page(self, *args, **kwargs):
407 return get_error_page(*args, **kwargs)
408
409 - def __call__(self):
410 """Use this exception as a request.handler (raise self).""" 411 raise self
412 413
414 -class NotFound(HTTPError):
415 416 """Exception raised when a URL could not be mapped to any handler (404). 417 418 This is equivalent to raising 419 :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`. 420 """ 421
422 - def __init__(self, path=None):
423 if path is None: 424 import cherrypy 425 request = cherrypy.serving.request 426 path = request.script_name + request.path_info 427 self.args = (path,) 428 HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
429 430 431 _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC 432 "-//W3C//DTD XHTML 1.0 Transitional//EN" 433 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 434 <html> 435 <head> 436 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta> 437 <title>%(status)s</title> 438 <style type="text/css"> 439 #powered_by { 440 margin-top: 20px; 441 border-top: 2px solid black; 442 font-style: italic; 443 } 444 445 #traceback { 446 color: red; 447 } 448 </style> 449 </head> 450 <body> 451 <h2>%(status)s</h2> 452 <p>%(message)s</p> 453 <pre id="traceback">%(traceback)s</pre> 454 <div id="powered_by"> 455 <span> 456 Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a> 457 </span> 458 </div> 459 </body> 460 </html> 461 ''' 462 463
464 -def get_error_page(status, **kwargs):
465 """Return an HTML page, containing a pretty error response. 466 467 status should be an int or a str. 468 kwargs will be interpolated into the page template. 469 """ 470 import cherrypy 471 472 try: 473 code, reason, message = _httputil.valid_status(status) 474 except ValueError: 475 raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) 476 477 # We can't use setdefault here, because some 478 # callers send None for kwarg values. 479 if kwargs.get('status') is None: 480 kwargs['status'] = "%s %s" % (code, reason) 481 if kwargs.get('message') is None: 482 kwargs['message'] = message 483 if kwargs.get('traceback') is None: 484 kwargs['traceback'] = '' 485 if kwargs.get('version') is None: 486 kwargs['version'] = cherrypy.__version__ 487 488 for k, v in iteritems(kwargs): 489 if v is None: 490 kwargs[k] = "" 491 else: 492 kwargs[k] = _escape(kwargs[k]) 493 494 # Use a custom template or callable for the error page? 495 pages = cherrypy.serving.request.error_page 496 error_page = pages.get(code) or pages.get('default') 497 498 # Default template, can be overridden below. 499 template = _HTTPErrorTemplate 500 if error_page: 501 try: 502 if hasattr(error_page, '__call__'): 503 # The caller function may be setting headers manually, 504 # so we delegate to it completely. We may be returning 505 # an iterator as well as a string here. 506 # 507 # We *must* make sure any content is not unicode. 508 result = error_page(**kwargs) 509 if cherrypy.lib.is_iterator(result): 510 from cherrypy.lib.encoding import UTF8StreamEncoder 511 return UTF8StreamEncoder(result) 512 elif isinstance(result, cherrypy._cpcompat.unicodestr): 513 return result.encode('utf-8') 514 else: 515 if not isinstance(result, cherrypy._cpcompat.bytestr): 516 raise ValueError('error page function did not ' 517 'return a bytestring, unicodestring or an ' 518 'iterator - returned object of type %s.' 519 % (type(result).__name__)) 520 return result 521 else: 522 # Load the template from this path. 523 template = tonative(open(error_page, 'rb').read()) 524 except: 525 e = _format_exception(*_exc_info())[-1] 526 m = kwargs['message'] 527 if m: 528 m += "<br />" 529 m += "In addition, the custom error page failed:\n<br />%s" % e 530 kwargs['message'] = m 531 532 response = cherrypy.serving.response 533 response.headers['Content-Type'] = "text/html;charset=utf-8" 534 result = template % kwargs 535 return result.encode('utf-8')
536 537 538 539 _ie_friendly_error_sizes = { 540 400: 512, 403: 256, 404: 512, 405: 256, 541 406: 512, 408: 512, 409: 512, 410: 256, 542 500: 512, 501: 512, 505: 512, 543 } 544 545
546 -def _be_ie_unfriendly(status):
547 import cherrypy 548 response = cherrypy.serving.response 549 550 # For some statuses, Internet Explorer 5+ shows "friendly error 551 # messages" instead of our response.body if the body is smaller 552 # than a given size. Fix this by returning a body over that size 553 # (by adding whitespace). 554 # See http://support.microsoft.com/kb/q218155/ 555 s = _ie_friendly_error_sizes.get(status, 0) 556 if s: 557 s += 1 558 # Since we are issuing an HTTP error status, we assume that 559 # the entity is short, and we should just collapse it. 560 content = response.collapse_body() 561 l = len(content) 562 if l and l < s: 563 # IN ADDITION: the response must be written to IE 564 # in one chunk or it will still get replaced! Bah. 565 content = content + (ntob(" ") * (s - l)) 566 response.body = content 567 response.headers['Content-Length'] = str(len(content))
568 569
570 -def format_exc(exc=None):
571 """Return exc (or sys.exc_info if None), formatted.""" 572 try: 573 if exc is None: 574 exc = _exc_info() 575 if exc == (None, None, None): 576 return "" 577 import traceback 578 return "".join(traceback.format_exception(*exc)) 579 finally: 580 del exc
581 582
583 -def bare_error(extrabody=None):
584 """Produce status, headers, body for a critical error. 585 586 Returns a triple without calling any other questionable functions, 587 so it should be as error-free as possible. Call it from an HTTP server 588 if you get errors outside of the request. 589 590 If extrabody is None, a friendly but rather unhelpful error message 591 is set in the body. If extrabody is a string, it will be appended 592 as-is to the body. 593 """ 594 595 # The whole point of this function is to be a last line-of-defense 596 # in handling errors. That is, it must not raise any errors itself; 597 # it cannot be allowed to fail. Therefore, don't add to it! 598 # In particular, don't call any other CP functions. 599 600 body = ntob("Unrecoverable error in the server.") 601 if extrabody is not None: 602 if not isinstance(extrabody, bytestr): 603 extrabody = extrabody.encode('utf-8') 604 body += ntob("\n") + extrabody 605 606 return (ntob("500 Internal Server Error"), 607 [(ntob('Content-Type'), ntob('text/plain')), 608 (ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))], 609 [body])
610