OLD | NEW |
(Empty) | |
| 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 sh
ould 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 se
t |
| 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></h
tml>"] |
| 97 sendMail('error@domain.com', 'Error in your web app', _cperror.format_ex
c()) |
| 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.R
esponse.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, u
rljoin 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 sta
tus |
| 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 Transitiona
l//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 |
| 556 |
OLD | NEW |