Removed spurious static_path.
[smonitor.git] / monitor / cherrypy / _cperror.py
blob00e5b532b58bff41228008ad3ddd0fe1b678f4cb
1 """Exception classes for CherryPy.
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>`.
10 .. _redirectingpost:
12 Redirecting POST
13 ================
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).
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!
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:
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 ===== ================================= ===========
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.
47 Custom Error Handling
48 =====================
50 .. image:: /refman/cperrors.gif
52 Anticipated HTTP responses
53 --------------------------
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>`_.
63 _cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
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::
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})
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.
79 Unanticipated errors
80 --------------------
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".
89 Here is some sample code that shows how to display a custom error message and
90 send an e-mail containing the error::
92 from cherrypy import _cperror
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())
99 class Root:
100 _cp_config = {'request.error_response': handle_error}
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.
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, iteritems, urljoin as _urljoin
111 from cherrypy.lib import httputil as _httputil
114 class CherryPyException(Exception):
115 """A base class for CherryPy exceptions."""
116 pass
119 class TimeoutError(CherryPyException):
120 """Exception raised when Response.timed_out is detected."""
121 pass
124 class InternalRedirect(CherryPyException):
125 """Exception raised to switch to the handler for a different URL.
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.
132 def __init__(self, path, query_string=""):
133 import cherrypy
134 self.request = cherrypy.serving.request
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)
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)
147 # Set a 'path' member attribute so that code which traps this
148 # error can have access to it.
149 self.path = path
151 CherryPyException.__init__(self, path, self.query_string)
154 class HTTPRedirect(CherryPyException):
155 """Exception raised when the request should be redirected.
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.
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.
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).
170 Examples::
172 raise cherrypy.HTTPRedirect("")
173 raise cherrypy.HTTPRedirect("/abs/path", 307)
174 raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
176 See :ref:`redirectingpost` for additional caveats.
179 status = None
180 """The integer HTTP status code to emit."""
182 urls = None
183 """The list of URL's to emit."""
185 encoding = 'utf-8'
186 """The encoding when passed urls are unicode objects"""
188 def __init__(self, urls, status=None, encoding=None):
189 import cherrypy
190 request = cherrypy.serving.request
192 if isinstance(urls, basestring):
193 urls = [urls]
195 abs_urls = []
196 for url in urls:
197 if isinstance(url, unicode):
198 url = url.encode(encoding or self.encoding)
200 # Note that urljoin will "do the right thing" whether url is:
201 # 1. a complete URL with host (e.g. "http://www.example.com/test")
202 # 2. a URL relative to root (e.g. "/dummy")
203 # 3. a URL relative to the current path
204 # Note that any query string in cherrypy.request is discarded.
205 url = _urljoin(cherrypy.url(), url)
206 abs_urls.append(url)
207 self.urls = abs_urls
209 # RFC 2616 indicates a 301 response code fits our goal; however,
210 # browser support for 301 is quite messy. Do 302/303 instead. See
211 # http://www.alanflavell.org.uk/www/post-redirect.html
212 if status is None:
213 if request.protocol >= (1, 1):
214 status = 303
215 else:
216 status = 302
217 else:
218 status = int(status)
219 if status < 300 or status > 399:
220 raise ValueError("status must be between 300 and 399.")
222 self.status = status
223 CherryPyException.__init__(self, abs_urls, status)
225 def set_response(self):
226 """Modify cherrypy.response status, headers, and body to represent self.
228 CherryPy uses this internally, but you can also use it to create an
229 HTTPRedirect object and set its output without *raising* the exception.
231 import cherrypy
232 response = cherrypy.serving.response
233 response.status = status = self.status
235 if status in (300, 301, 302, 303, 307):
236 response.headers['Content-Type'] = "text/html;charset=utf-8"
237 # "The ... URI SHOULD be given by the Location field
238 # in the response."
239 response.headers['Location'] = self.urls[0]
241 # "Unless the request method was HEAD, the entity of the response
242 # SHOULD contain a short hypertext note with a hyperlink to the
243 # new URI(s)."
244 msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
245 301: "This resource has permanently moved to <a href='%s'>%s</a>.",
246 302: "This resource resides temporarily at <a href='%s'>%s</a>.",
247 303: "This resource can be found at <a href='%s'>%s</a>.",
248 307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
249 }[status]
250 msgs = [msg % (u, u) for u in self.urls]
251 response.body = "<br />\n".join(msgs)
252 # Previous code may have set C-L, so we have to reset it
253 # (allow finalize to set it).
254 response.headers.pop('Content-Length', None)
255 elif status == 304:
256 # Not Modified.
257 # "The response MUST include the following header fields:
258 # Date, unless its omission is required by section 14.18.1"
259 # The "Date" header should have been set in Response.__init__
261 # "...the response SHOULD NOT include other entity-headers."
262 for key in ('Allow', 'Content-Encoding', 'Content-Language',
263 'Content-Length', 'Content-Location', 'Content-MD5',
264 'Content-Range', 'Content-Type', 'Expires',
265 'Last-Modified'):
266 if key in response.headers:
267 del response.headers[key]
269 # "The 304 response MUST NOT contain a message-body."
270 response.body = None
271 # Previous code may have set C-L, so we have to reset it.
272 response.headers.pop('Content-Length', None)
273 elif status == 305:
274 # Use Proxy.
275 # self.urls[0] should be the URI of the proxy.
276 response.headers['Location'] = self.urls[0]
277 response.body = None
278 # Previous code may have set C-L, so we have to reset it.
279 response.headers.pop('Content-Length', None)
280 else:
281 raise ValueError("The %s status code is unknown." % status)
283 def __call__(self):
284 """Use this exception as a request.handler (raise self)."""
285 raise self
288 def clean_headers(status):
289 """Remove any headers which should not apply to an error response."""
290 import cherrypy
292 response = cherrypy.serving.response
294 # Remove headers which applied to the original content,
295 # but do not apply to the error page.
296 respheaders = response.headers
297 for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
298 "Vary", "Content-Encoding", "Content-Length", "Expires",
299 "Content-Location", "Content-MD5", "Last-Modified"]:
300 if key in respheaders:
301 del respheaders[key]
303 if status != 416:
304 # A server sending a response with status code 416 (Requested
305 # range not satisfiable) SHOULD include a Content-Range field
306 # with a byte-range-resp-spec of "*". The instance-length
307 # specifies the current length of the selected resource.
308 # A response with status code 206 (Partial Content) MUST NOT
309 # include a Content-Range field with a byte-range- resp-spec of "*".
310 if "Content-Range" in respheaders:
311 del respheaders["Content-Range"]
314 class HTTPError(CherryPyException):
315 """Exception used to return an HTTP error code (4xx-5xx) to the client.
317 This exception can be used to automatically send a response using a http status
318 code, with an appropriate error page. It takes an optional
319 ``status`` argument (which must be between 400 and 599); it defaults to 500
320 ("Internal Server Error"). It also takes an optional ``message`` argument,
321 which will be returned in the response body. See
322 `RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
323 for a complete list of available error codes and when to use them.
325 Examples::
327 raise cherrypy.HTTPError(403)
328 raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
331 status = None
332 """The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
334 code = None
335 """The integer HTTP status code."""
337 reason = None
338 """The HTTP Reason-Phrase string."""
340 def __init__(self, status=500, message=None):
341 self.status = status
342 try:
343 self.code, self.reason, defaultmsg = _httputil.valid_status(status)
344 except ValueError, x:
345 raise self.__class__(500, x.args[0])
347 if self.code < 400 or self.code > 599:
348 raise ValueError("status must be between 400 and 599.")
350 # See http://www.python.org/dev/peps/pep-0352/
351 # self.message = message
352 self._message = message or defaultmsg
353 CherryPyException.__init__(self, status, message)
355 def set_response(self):
356 """Modify cherrypy.response status, headers, and body to represent self.
358 CherryPy uses this internally, but you can also use it to create an
359 HTTPError object and set its output without *raising* the exception.
361 import cherrypy
363 response = cherrypy.serving.response
365 clean_headers(self.code)
367 # In all cases, finalize will be called after this method,
368 # so don't bother cleaning up response values here.
369 response.status = self.status
370 tb = None
371 if cherrypy.serving.request.show_tracebacks:
372 tb = format_exc()
373 response.headers['Content-Type'] = "text/html;charset=utf-8"
374 response.headers.pop('Content-Length', None)
376 content = self.get_error_page(self.status, traceback=tb,
377 message=self._message)
378 response.body = content
380 _be_ie_unfriendly(self.code)
382 def get_error_page(self, *args, **kwargs):
383 return get_error_page(*args, **kwargs)
385 def __call__(self):
386 """Use this exception as a request.handler (raise self)."""
387 raise self
390 class NotFound(HTTPError):
391 """Exception raised when a URL could not be mapped to any handler (404).
393 This is equivalent to raising
394 :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
397 def __init__(self, path=None):
398 if path is None:
399 import cherrypy
400 request = cherrypy.serving.request
401 path = request.script_name + request.path_info
402 self.args = (path,)
403 HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
406 _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
407 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
408 <html>
409 <head>
410 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
411 <title>%(status)s</title>
412 <style type="text/css">
413 #powered_by {
414 margin-top: 20px;
415 border-top: 2px solid black;
416 font-style: italic;
419 #traceback {
420 color: red;
422 </style>
423 </head>
424 <body>
425 <h2>%(status)s</h2>
426 <p>%(message)s</p>
427 <pre id="traceback">%(traceback)s</pre>
428 <div id="powered_by">
429 <span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
430 </div>
431 </body>
432 </html>
435 def get_error_page(status, **kwargs):
436 """Return an HTML page, containing a pretty error response.
438 status should be an int or a str.
439 kwargs will be interpolated into the page template.
441 import cherrypy
443 try:
444 code, reason, message = _httputil.valid_status(status)
445 except ValueError, x:
446 raise cherrypy.HTTPError(500, x.args[0])
448 # We can't use setdefault here, because some
449 # callers send None for kwarg values.
450 if kwargs.get('status') is None:
451 kwargs['status'] = "%s %s" % (code, reason)
452 if kwargs.get('message') is None:
453 kwargs['message'] = message
454 if kwargs.get('traceback') is None:
455 kwargs['traceback'] = ''
456 if kwargs.get('version') is None:
457 kwargs['version'] = cherrypy.__version__
459 for k, v in iteritems(kwargs):
460 if v is None:
461 kwargs[k] = ""
462 else:
463 kwargs[k] = _escape(kwargs[k])
465 # Use a custom template or callable for the error page?
466 pages = cherrypy.serving.request.error_page
467 error_page = pages.get(code) or pages.get('default')
468 if error_page:
469 try:
470 if hasattr(error_page, '__call__'):
471 return error_page(**kwargs)
472 else:
473 return open(error_page, 'rb').read() % 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
482 return _HTTPErrorTemplate % kwargs
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,
492 def _be_ie_unfriendly(status):
493 import cherrypy
494 response = cherrypy.serving.response
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 + (" " * (s - l))
512 response.body = content
513 response.headers['Content-Length'] = str(len(content))
516 def format_exc(exc=None):
517 """Return exc (or sys.exc_info if None), formatted."""
518 if exc is None:
519 exc = _exc_info()
520 if exc == (None, None, None):
521 return ""
522 import traceback
523 return "".join(traceback.format_exception(*exc))
525 def bare_error(extrabody=None):
526 """Produce status, headers, body for a critical error.
528 Returns a triple without calling any other questionable functions,
529 so it should be as error-free as possible. Call it from an HTTP server
530 if you get errors outside of the request.
532 If extrabody is None, a friendly but rather unhelpful error message
533 is set in the body. If extrabody is a string, it will be appended
534 as-is to the body.
537 # The whole point of this function is to be a last line-of-defense
538 # in handling errors. That is, it must not raise any errors itself;
539 # it cannot be allowed to fail. Therefore, don't add to it!
540 # In particular, don't call any other CP functions.
542 body = "Unrecoverable error in the server."
543 if extrabody is not None:
544 if not isinstance(extrabody, str):
545 extrabody = extrabody.encode('utf-8')
546 body += "\n" + extrabody
548 return ("500 Internal Server Error",
549 [('Content-Type', 'text/plain'),
550 ('Content-Length', str(len(body)))],
551 [body])