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
115 """A base class for CherryPy exceptions."""
116 pass
117
118
120 """Exception raised when Response.timed_out is detected."""
121 pass
122
123
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=""):
152
153
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):
223
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
237
238 response.headers['Location'] = self.urls[0]
239
240
241
242
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
252
253 response.headers.pop('Content-Length', None)
254 elif status == 304:
255
256
257
258
259
260
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
269 response.body = None
270
271 response.headers.pop('Content-Length', None)
272 elif status == 305:
273
274
275 response.headers['Location'] = self.urls[0]
276 response.body = None
277
278 response.headers.pop('Content-Length', None)
279 else:
280 raise ValueError("The %s status code is unknown." % status)
281
283 """Use this exception as a request.handler (raise self)."""
284 raise self
285
286
288 """Remove any headers which should not apply to an error response."""
289 import cherrypy
290
291 response = cherrypy.serving.response
292
293
294
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
304
305
306
307
308
309 if "Content-Range" in respheaders:
310 del respheaders["Content-Range"]
311
312
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):
353
380
381 - def get_error_page(self, *args, **kwargs):
383
385 """Use this exception as a request.handler (raise self)."""
386 raise self
387
388
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
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
448
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
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
514
515
527
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
541
542
543
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