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

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

Revision 1540 (checked in by fumanchu, 3 years ago)

Minor cleanups.

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets