source file: /Library/Python/2.3/site-packages/CherryPy-3.0.1-py2.3.egg/cherrypy/_cperror.py
file stats: 150 lines, 31 executed: 20.7% covered
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.