Package cherrypy :: Module _cperror
[hide private]
[frames] | no frames]

Source Code for Module cherrypy._cperror

  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="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