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