Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/_cperror.py

Revision 1949 (checked in by fumanchu, 3 months ago)

Fix for #800 (ability to override default error template). Many thanks to Scott Chapman for the ideas and Nicolas Grilly for the fix.

  • Property svn:eol-style set to native
Line 
1 """Error classes for CherryPy."""
2
3 from cgi import escape as _escape
4 from sys import exc_info as _exc_info
5 from traceback import format_exception as _format_exception
6 from urlparse import urljoin as _urljoin
7 from cherrypy.lib import http as _http
8
9
10 class CherryPyException(Exception):
11     pass
12
13
14 class TimeoutError(CherryPyException):
15     """Exception raised when Response.timed_out is detected."""
16     pass
17
18
19 class InternalRedirect(CherryPyException):
20     """Exception raised to switch to the handler for a different URL.
21     
22     Any request.params must be supplied in a query string.
23     """
24    
25     def __init__(self, path):
26         import cherrypy
27         request = cherrypy.request
28        
29         self.query_string = ""
30         if "?" in path:
31             # Separate any params included in the path
32             path, self.query_string = path.split("?", 1)
33        
34         # Note that urljoin will "do the right thing" whether url is:
35         #  1. a URL relative to root (e.g. "/dummy")
36         #  2. a URL relative to the current path
37         # Note that any query string will be discarded.
38         path = _urljoin(request.path_info, path)
39        
40         # Set a 'path' member attribute so that code which traps this
41         # error can have access to it.
42         self.path = path
43        
44         CherryPyException.__init__(self, path, self.query_string)
45
46
47 class HTTPRedirect(CherryPyException):
48     """Exception raised when the request should be redirected.
49     
50     The new URL must be passed as the first argument to the Exception,
51     e.g., HTTPRedirect(newUrl). Multiple URLs are allowed. If a URL is
52     absolute, it will be used as-is. If it is relative, it is assumed
53     to be relative to the current cherrypy.request.path_info.
54     """
55    
56     def __init__(self, urls, status=None):
57         import cherrypy
58         request = cherrypy.request
59        
60         if isinstance(urls, basestring):
61             urls = [urls]
62        
63         abs_urls = []
64         for url in urls:
65             # Note that urljoin will "do the right thing" whether url is:
66             #  1. a complete URL with host (e.g. "http://www.example.com/test")
67             #  2. a URL relative to root (e.g. "/dummy")
68             #  3. a URL relative to the current path
69             # Note that any query string in cherrypy.request is discarded.
70             url = _urljoin(cherrypy.url(), url)
71             abs_urls.append(url)
72         self.urls = abs_urls
73        
74         # RFC 2616 indicates a 301 response code fits our goal; however,
75         # browser support for 301 is quite messy. Do 302/303 instead. See
76         # http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
77         if status is None:
78             if request.protocol >= (1, 1):
79                 status = 303
80             else:
81                 status = 302
82         else:
83             status = int(status)
84             if status < 300 or status > 399:
85                 raise ValueError("status must be between 300 and 399.")
86        
87         self.status = status
88         CherryPyException.__init__(self, abs_urls, status)
89    
90     def set_response(self):
91         """Modify cherrypy.response status, headers, and body to represent self.
92         
93         CherryPy uses this internally, but you can also use it to create an
94         HTTPRedirect object and set its output without *raising* the exception.
95         """
96         import cherrypy
97         response = cherrypy.response
98         response.status = status = self.status
99        
100         if status in (300, 301, 302, 303, 307):
101             response.headers['Content-Type'] = "text/html"
102             # "The ... URI SHOULD be given by the Location field
103             # in the response."
104             response.headers['Location'] = self.urls[0]
105            
106             # "Unless the request method was HEAD, the entity of the response
107             # SHOULD contain a short hypertext note with a hyperlink to the
108             # new URI(s)."
109             msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
110                    301: "This resource has permanently moved to <a href='%s'>%s</a>.",
111                    302: "This resource resides temporarily at <a href='%s'>%s</a>.",
112                    303: "This resource can be found at <a href='%s'>%s</a>.",
113                    307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
114                    }[status]
115             response.body = "<br />\n".join([msg % (u, u) for u in self.urls])
116             # Previous code may have set C-L, so we have to reset it
117             # (allow finalize to set it).
118             response.headers.pop('Content-Length', None)
119         elif status == 304:
120             # Not Modified.
121             # "The response MUST include the following header fields:
122             # Date, unless its omission is required by section 14.18.1"
123             # The "Date" header should have been set in Response.__init__
124            
125             # "...the response SHOULD NOT include other entity-headers."
126             for key in ('Allow', 'Content-Encoding', 'Content-Language',
127                         'Content-Length', 'Content-Location', 'Content-MD5',
128                         'Content-Range', 'Content-Type', 'Expires',
129                         'Last-Modified'):
130                 if key in response.headers:
131                     del response.headers[key]
132            
133             # "The 304 response MUST NOT contain a message-body."
134             response.body = None
135             # Previous code may have set C-L, so we have to reset it.
136             response.headers.pop('Content-Length', None)
137         elif status == 305:
138             # Use Proxy.
139             # self.urls[0] should be the URI of the proxy.
140             response.headers['Location'] = self.urls[0]
141             response.body = None
142             # Previous code may have set C-L, so we have to reset it.
143             response.headers.pop('Content-Length', None)
144         else:
145             raise ValueError("The %s status code is unknown." % status)
146    
147     def __call__(self):
148         """Use this exception as a request.handler (raise self)."""
149         raise self
150
151
152 def clean_headers(status):
153     """Remove any headers which should not apply to an error response."""
154     import cherrypy
155    
156     response = cherrypy.response
157    
158     # Remove headers which applied to the original content,
159     # but do not apply to the error page.
160     respheaders = response.headers
161     for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
162                 "Vary", "Content-Encoding", "Content-Length", "Expires",
163                 "Content-Location", "Content-MD5", "Last-Modified"]:
164         if respheaders.has_key(key):
165             del respheaders[key]
166    
167     if status != 416:
168         # A server sending a response with status code 416 (Requested
169         # range not satisfiable) SHOULD include a Content-Range field
170         # with a byte-range-resp-spec of "*". The instance-length
171         # specifies the current length of the selected resource.
172         # A response with status code 206 (Partial Content) MUST NOT
173         # include a Content-Range field with a byte-range- resp-spec of "*".
174         if respheaders.has_key("Content-Range"):
175             del respheaders["Content-Range"]
176
177
178 class HTTPError(CherryPyException):
179     """ Exception used to return an HTTP error code (4xx-5xx) to the client.
180         This exception will automatically set the response status and body.
181         
182         A custom message (a long description to display in the browser)
183         can be provided in place of the default.
184     """
185    
186     def __init__(self, status=500, message=None):
187         self.status = status = int(status)
188         if status < 400 or status > 599:
189             raise ValueError("status must be between 400 and 599.")
190         self.message = message
191         CherryPyException.__init__(self, status, message)
192    
193     def set_response(self):
194         """Modify cherrypy.response status, headers, and body to represent self.
195         
196         CherryPy uses this internally, but you can also use it to create an
197         HTTPError object and set its output without *raising* the exception.
198         """
199         import cherrypy
200        
201         response = cherrypy.response
202        
203         clean_headers(self.status)
204        
205         # In all cases, finalize will be called after this method,
206         # so don't bother cleaning up response values here.
207         response.status = self.status
208         tb = None
209         if cherrypy.request.show_tracebacks:
210             tb = format_exc()
211         response.headers['Content-Type'] = "text/html"
212        
213         content = self.get_error_page(self.status, traceback=tb,
214                                       message=self.message)
215         response.body = content
216         response.headers['Content-Length'] = len(content)
217        
218         _be_ie_unfriendly(self.status)
219    
220     def get_error_page(self, *args, **kwargs):
221         return get_error_page(*args, **kwargs)
222    
223     def __call__(self):
224         """Use this exception as a request.handler (raise self)."""
225         raise self
226
227
228 class NotFound(HTTPError):
229     """Exception raised when a URL could not be mapped to any handler (404)."""
230    
231     def __init__(self, path=None):
232         if path is None:
233             import cherrypy
234             path = cherrypy.request.script_name + cherrypy.request.path_info
235         self.args = (path,)
236         HTTPError.__init__(self, 404, "The path %r was not found." % path)
237
238
239 _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
240 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
241 <html>
242 <head>
243     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
244     <title>%(status)s</title>
245     <style type="text/css">
246     #powered_by {
247         margin-top: 20px;
248         border-top: 2px solid black;
249         font-style: italic;
250     }
251
252     #traceback {
253         color: red;
254     }
255     </style>
256 </head>
257     <body>
258         <h2>%(status)s</h2>
259         <p>%(message)s</p>
260         <pre id="traceback">%(traceback)s</pre>
261     <div id="powered_by">
262     <span>Powered by <a href="CherryPy">http://www.cherrypy.org">CherryPy %(version)s</a></span>
263     </div>
264     </body>
265 </html>
266 '''
267
268 def get_error_page(status, **kwargs):
269     """Return an HTML page, containing a pretty error response.
270     
271     status should be an int or a str.
272     kwargs will be interpolated into the page template.
273     """
274     import cherrypy
275    
276     try:
277         code, reason, message = _http.valid_status(status)
278     except ValueError, x:
279         raise cherrypy.HTTPError(500, x.args[0])
280    
281     # We can't use setdefault here, because some
282     # callers send None for kwarg values.
283     if kwargs.get('status') is None:
284         kwargs['status'] = "%s %s" % (code, reason)
285     if kwargs.get('message') is None:
286         kwargs['message'] = message
287     if kwargs.get('traceback') is None:
288         kwargs['traceback'] = ''
289     if kwargs.get('version') is None:
290         kwargs['version'] = cherrypy.__version__
291    
292     for k, v in kwargs.iteritems():
293         if v is None:
294             kwargs[k] = ""
295         else:
296             kwargs[k] = _escape(kwargs[k])
297    
298     # Use a custom template or callable for the error page?
299     pages = cherrypy.request.error_page
300     error_page = pages.get(code) or pages.get('default')
301     if error_page:
302         try:
303             if callable(error_page):
304                 return error_page(**kwargs)
305             else:
306                 return file(error_page, 'rb').read() % kwargs
307         except:
308             e = _format_exception(*_exc_info())[-1]
309             m = kwargs['message']
310             if m:
311                 m += "<br />"
312             m += "In addition, the custom error page failed:\n<br />%s" % e
313             kwargs['message'] = m
314    
315     return _HTTPErrorTemplate % kwargs
316
317
318 _ie_friendly_error_sizes = {
319     400: 512, 403: 256, 404: 512, 405: 256,
320     406: 512, 408: 512, 409: 512, 410: 256,
321     500: 512, 501: 512, 505: 512,
322     }
323
324
325 def _be_ie_unfriendly(status):
326     import cherrypy
327     response = cherrypy.response
328    
329     # For some statuses, Internet Explorer 5+ shows "friendly error
330     # messages" instead of our response.body if the body is smaller
331     # than a given size. Fix this by returning a body over that size
332     # (by adding whitespace).
333     # See http://support.microsoft.com/kb/q218155/
334     s = _ie_friendly_error_sizes.get(status, 0)
335     if s:
336         s += 1
337         # Since we are issuing an HTTP error status, we assume that
338         # the entity is short, and we should just collapse it.
339         content = response.collapse_body()
340         l = len(content)
341         if l and l < s:
342             # IN ADDITION: the response must be written to IE
343             # in one chunk or it will still get replaced! Bah.
344             content = content + (" " * (s - l))
345         response.body = content
346         response.headers['Content-Length'] = len(content)
347
348
349 def format_exc(exc=None):
350     """Return exc (or sys.exc_info if None), formatted."""
351     if exc is None:
352         exc = _exc_info()
353     if exc == (None, None, None):
354         return ""
355     import traceback
356     return "".join(traceback.format_exception(*exc))
357
358 def bare_error(extrabody=None):
359     """Produce status, headers, body for a critical error.
360     
361     Returns a triple without calling any other questionable functions,
362     so it should be as error-free as possible. Call it from an HTTP server
363     if you get errors outside of the request.
364     
365     If extrabody is None, a friendly but rather unhelpful error message
366     is set in the body. If extrabody is a string, it will be appended
367     as-is to the body.
368     """
369    
370     # The whole point of this function is to be a last line-of-defense
371     # in handling errors. That is, it must not raise any errors itself;
372     # it cannot be allowed to fail. Therefore, don't add to it!
373     # In particular, don't call any other CP functions.
374    
375     body = "Unrecoverable error in the server."
376     if extrabody is not None:
377         body += "\n" + extrabody
378    
379     return ("500 Internal Server Error",
380             [('Content-Type', 'text/plain'),
381              ('Content-Length', str(len(body)))],
382             [body])
383
384
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets