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
127
128 """A base class for CherryPy exceptions."""
129 pass
130
131
133
134 """Exception raised when Response.timed_out is detected."""
135 pass
136
137
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=""):
168
169
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):
240
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
255
256 response.headers['Location'] = self.urls[0]
257
258
259
260
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
273
274 response.headers.pop('Content-Length', None)
275 elif status == 304:
276
277
278
279
280
281
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
290 response.body = None
291
292 response.headers.pop('Content-Length', None)
293 elif status == 305:
294
295
296 response.headers['Location'] = self.urls[0]
297 response.body = None
298
299 response.headers.pop('Content-Length', None)
300 else:
301 raise ValueError("The %s status code is unknown." % status)
302
304 """Use this exception as a request.handler (raise self)."""
305 raise self
306
307
309 """Remove any headers which should not apply to an error response."""
310 import cherrypy
311
312 response = cherrypy.serving.response
313
314
315
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
325
326
327
328
329
330 if "Content-Range" in respheaders:
331 del respheaders["Content-Range"]
332
333
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):
377
405
406 - def get_error_page(self, *args, **kwargs):
408
410 """Use this exception as a request.handler (raise self)."""
411 raise self
412
413
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
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
478
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
495 pages = cherrypy.serving.request.error_page
496 error_page = pages.get(code) or pages.get('default')
497
498
499 template = _HTTPErrorTemplate
500 if error_page:
501 try:
502 if hasattr(error_page, '__call__'):
503
504
505
506
507
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
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
568
569
581
582
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
596
597
598
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