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

root/tags/cherrypy-3.0.3/cherrypy/_cperror.py

Revision 1617 (checked in by jtate, 2 years ago)

Add info on running individual tests to the README

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets