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

root/tags/cherrypy-3.1.0beta2/cherrypy/_cperror.py

Revision 1750 (checked in by fumanchu, 1 year ago)

Added an HTTPError.get_error_page for easier overriding.

  • 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         elif status == 304:
116             # Not Modified.
117             # "The response MUST include the following header fields:
118             # Date, unless its omission is required by section 14.18.1"
119             # The "Date" header should have been set in Response.__init__
120            
121             # "...the response SHOULD NOT include other entity-headers."
122             for key in ('Allow', 'Content-Encoding', 'Content-Language',
123                         'Content-Length', 'Content-Location', 'Content-MD5',
124                         'Content-Range', 'Content-Type', 'Expires',
125                         'Last-Modified'):
126                 if key in response.headers:
127                     del response.headers[key]
128            
129             # "The 304 response MUST NOT contain a message-body."
130             response.body = None
131         elif status == 305:
132             # Use Proxy.
133             # self.urls[0] should be the URI of the proxy.
134             response.headers['Location'] = self.urls[0]
135             response.body = None
136         else:
137             raise ValueError("The %s status code is unknown." % status)
138    
139     def __call__(self):
140         """Use this exception as a request.handler (raise self)."""
141         raise self
142
143
144 class HTTPError(CherryPyException):
145     """ Exception used to return an HTTP error code (4xx-5xx) to the client.
146         This exception will automatically set the response status and body.
147         
148         A custom message (a long description to display in the browser)
149         can be provided in place of the default.
150     """
151    
152     def __init__(self, status=500, message=None):
153         self.status = status = int(status)
154         if status < 400 or status > 599:
155             raise ValueError("status must be between 400 and 599.")
156         self.message = message
157         CherryPyException.__init__(self, status, message)
158    
159     def set_response(self):
160         """Modify cherrypy.response status, headers, and body to represent self.
161         
162         CherryPy uses this internally, but you can also use it to create an
163         HTTPError object and set its output without *raising* the exception.
164         """
165         import cherrypy
166        
167         response = cherrypy.response
168        
169         # Remove headers which applied to the original content,
170         # but do not apply to the error page.
171         respheaders = response.headers
172         for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
173                     "Vary", "Content-Encoding", "Content-Length", "Expires",
174                     "Content-Location", "Content-MD5", "Last-Modified"]:
175             if respheaders.has_key(key):
176                 del respheaders[key]
177        
178         if self.status != 416:
179             # A server sending a response with status code 416 (Requested
180             # range not satisfiable) SHOULD include a Content-Range field
181             # with a byte-range- resp-spec of "*". The instance-length
182             # specifies the current length of the selected resource.
183             # A response with status code 206 (Partial Content) MUST NOT
184             # include a Content-Range field with a byte-range- resp-spec of "*".
185             if respheaders.has_key("Content-Range"):
186                 del respheaders["Content-Range"]
187        
188         # In all cases, finalize will be called after this method,
189         # so don't bother cleaning up response values here.
190         response.status = self.status
191         tb = None
192         if cherrypy.request.show_tracebacks:
193             tb = format_exc()
194         respheaders['Content-Type'] = "text/html"
195        
196         content = self.get_error_page(self.status, traceback=tb,
197                                       message=self.message)
198         response.body = content
199         respheaders['Content-Length'] = len(content)
200        
201         _be_ie_unfriendly(self.status)
202    
203     def get_error_page(self, *args, **kwargs):
204         return get_error_page(*args, **kwargs)
205    
206     def __call__(self):
207         """Use this exception as a request.handler (raise self)."""
208         raise self
209
210
211 class NotFound(HTTPError):
212     """Exception raised when a URL could not be mapped to any handler (404)."""
213    
214     def __init__(self, path=None):
215         if path is None:
216             import cherrypy
217             path = cherrypy.request.script_name + cherrypy.request.path_info
218         self.args = (path,)
219         HTTPError.__init__(self, 404, "The path %r was not found." % path)
220
221
222 _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
223 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
224 <html>
225 <head>
226     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
227     <title>%(status)s</title>
228     <style type="text/css">
229     #powered_by {
230         margin-top: 20px;
231         border-top: 2px solid black;
232         font-style: italic;
233     }
234
235     #traceback {
236         color: red;
237     }
238     </style>
239 </head>
240     <body>
241         <h2>%(status)s</h2>
242         <p>%(message)s</p>
243         <pre id="traceback">%(traceback)s</pre>
244     <div id="powered_by">
245     <span>Powered by <a href="CherryPy">http://www.cherrypy.org">CherryPy %(version)s</a></span>
246     </div>
247     </body>
248 </html>
249 '''
250
251 def get_error_page(status, **kwargs):
252     """Return an HTML page, containing a pretty error response.
253     
254     status should be an int or a str.
255     kwargs will be interpolated into the page template.
256     """
257     import cherrypy
258    
259     try:
260         code, reason, message = _http.valid_status(status)
261     except ValueError, x:
262         raise cherrypy.HTTPError(500, x.args[0])
263    
264     # We can't use setdefault here, because some
265     # callers send None for kwarg values.
266     if kwargs.get('status') is None:
267         kwargs['status'] = "%s %s" % (code, reason)
268     if kwargs.get('message') is None:
269         kwargs['message'] = message
270     if kwargs.get('traceback') is None:
271         kwargs['traceback'] = ''
272     if kwargs.get('version') is None:
273         kwargs['version'] = cherrypy.__version__
274     for k, v in kwargs.iteritems():
275         if v is None:
276             kwargs[k] = ""
277         else:
278             kwargs[k] = _escape(kwargs[k])
279    
280     template = _HTTPErrorTemplate
281    
282     # Replace the default template with a custom one?
283     error_page_file = cherrypy.request.error_page.get(code, '')
284     if error_page_file:
285         try:
286             template = file(error_page_file, 'rb').read()
287         except:
288             m = kwargs['message']
289             if m:
290                 m += "<br />"
291             m += ("In addition, the custom error page "
292                   "failed:\n<br />%s" % (_exc_info()[1]))
293             kwargs['message'] = m
294    
295     return template % kwargs
296
297
298 _ie_friendly_error_sizes = {
299     400: 512, 403: 256, 404: 512, 405: 256,
300     406: 512, 408: 512, 409: 512, 410: 256,
301     500: 512, 501: 512, 505: 512,
302     }
303
304
305 def _be_ie_unfriendly(status):
306     import cherrypy
307     response = cherrypy.response
308    
309     # For some statuses, Internet Explorer 5+ shows "friendly error
310     # messages" instead of our response.body if the body is smaller
311     # than a given size. Fix this by returning a body over that size
312     # (by adding whitespace).
313     # See http://support.microsoft.com/kb/q218155/
314     s = _ie_friendly_error_sizes.get(status, 0)
315     if s:
316         s += 1
317         # Since we are issuing an HTTP error status, we assume that
318         # the entity is short, and we should just collapse it.
319         content = response.collapse_body()
320         l = len(content)
321         if l and l < s:
322             # IN ADDITION: the response must be written to IE
323             # in one chunk or it will still get replaced! Bah.
324             content = content + (" " * (s - l))
325         response.body = content
326         response.headers['Content-Length'] = len(content)
327
328
329 def format_exc(exc=None):
330     """Return exc (or sys.exc_info if None), formatted."""
331     if exc is None:
332         exc = _exc_info()
333     if exc == (None, None, None):
334         return ""
335     import traceback
336     return "".join(traceback.format_exception(*exc))
337
338 def bare_error(extrabody=None):
339     """Produce status, headers, body for a critical error.
340     
341     Returns a triple without calling any other questionable functions,
342     so it should be as error-free as possible. Call it from an HTTP server
343     if you get errors outside of the request.
344     
345     If extrabody is None, a friendly but rather unhelpful error message
346     is set in the body. If extrabody is a string, it will be appended
347     as-is to the body.
348     """
349    
350     # The whole point of this function is to be a last line-of-defense
351     # in handling errors. That is, it must not raise any errors itself;
352     # it cannot be allowed to fail. Therefore, don't add to it!
353     # In particular, don't call any other CP functions.
354    
355     body = "Unrecoverable error in the server."
356     if extrabody is not None:
357         body += "\n" + extrabody
358    
359     return ("500 Internal Server Error",
360             [('Content-Type', 'text/plain'),
361              ('Content-Length', str(len(body)))],
362             [body])
363
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets