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

root/branches/598-sendall/cherrypy/_cperror.py

Revision 1839 (checked in by fumanchu, 11 months ago)

Fixed some problems with redirect after staticdir.

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets