Commits

Robert Brewer committed 7d4b3a9

Partial fix for #800 (). I'm not yet happy with making error handler authors do all the hard work: spec conformance, be IE unfriendly, even manually set the status, etc.

Comments (0)

Files changed (3)

cherrypy/_cperror.py

         raise self
 
 
+def clean_headers(status):
+    """Remove any headers which should not apply to an error response."""
+    import cherrypy
+    
+    response = cherrypy.response
+    
+    # Remove headers which applied to the original content,
+    # but do not apply to the error page.
+    respheaders = response.headers
+    for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
+                "Vary", "Content-Encoding", "Content-Length", "Expires",
+                "Content-Location", "Content-MD5", "Last-Modified"]:
+        if respheaders.has_key(key):
+            del respheaders[key]
+    
+    if status != 416:
+        # A server sending a response with status code 416 (Requested
+        # range not satisfiable) SHOULD include a Content-Range field
+        # with a byte-range-resp-spec of "*". The instance-length
+        # specifies the current length of the selected resource.
+        # A response with status code 206 (Partial Content) MUST NOT
+        # include a Content-Range field with a byte-range- resp-spec of "*".
+        if respheaders.has_key("Content-Range"):
+            del respheaders["Content-Range"]
+
+
 class HTTPError(CherryPyException):
     """ Exception used to return an HTTP error code (4xx-5xx) to the client.
         This exception will automatically set the response status and body.
         
         response = cherrypy.response
         
-        # Remove headers which applied to the original content,
-        # but do not apply to the error page.
-        respheaders = response.headers
-        for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
-                    "Vary", "Content-Encoding", "Content-Length", "Expires",
-                    "Content-Location", "Content-MD5", "Last-Modified"]:
-            if respheaders.has_key(key):
-                del respheaders[key]
-        
-        if self.status != 416:
-            # A server sending a response with status code 416 (Requested
-            # range not satisfiable) SHOULD include a Content-Range field
-            # with a byte-range-resp-spec of "*". The instance-length
-            # specifies the current length of the selected resource.
-            # A response with status code 206 (Partial Content) MUST NOT
-            # include a Content-Range field with a byte-range- resp-spec of "*".
-            if respheaders.has_key("Content-Range"):
-                del respheaders["Content-Range"]
+        clean_headers(self.status)
         
         # In all cases, finalize will be called after this method,
         # so don't bother cleaning up response values here.
         tb = None
         if cherrypy.request.show_tracebacks:
             tb = format_exc()
-        respheaders['Content-Type'] = "text/html"
+        response.headers['Content-Type'] = "text/html"
         
         content = self.get_error_page(self.status, traceback=tb,
                                       message=self.message)
         response.body = content
-        respheaders['Content-Length'] = len(content)
+        response.headers['Content-Length'] = len(content)
         
         _be_ie_unfriendly(self.status)
     

cherrypy/_cprequest.py

     
     error_page = {}
     error_page__doc = """
-    A dict of {error code: response filename} pairs. The named response
-    files should be Python string-formatting templates, and can expect by
-    default to receive the format values with the mapping keys 'status',
-    'message', 'traceback', and 'version'. The set of format mappings
-    can be extended by overriding HTTPError.set_response."""
+    A dict of {error code: response filename or callable} pairs.
+    The named response files should be Python string-formatting templates,
+    and can expect by default to receive format values with the mapping
+    keys 'status', 'message', 'traceback', and 'version'. The set of
+    format mappings can be extended by overriding HTTPError.set_response.
+    
+    If a callable is provided, it will be called with the HTTPError
+    instance as the only argument. The callable is expected to set
+    response.status, .headers and .body appropriately.
+    
+    If no entry is given for an error code, the HTTPError's set_response
+    method will handle the error (by setting .status, .headers, and .body).
+    """
     
     show_tracebacks = True
     show_tracebacks__doc = """
                     self.hooks.run('before_finalize')
                     cherrypy.response.finalize()
                 except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
-                    inst.set_response()
+                    ep = self.error_page.get(inst.status, '')
+                    if ep and callable(ep):
+                        ep(inst)
+                    else:
+                        inst.set_response()
                     self.stage = 'before_finalize (HTTPError)'
                     self.hooks.run('before_finalize')
                     cherrypy.response.finalize()
             self.params.update(p)
     
     def handle_error(self, exc):
-        """Handle the last exception. (Core)"""
+        """Handle the last unanticipated exception. (Core)"""
         try:
             self.hooks.run("before_error_response")
             if self.error_response:
             self.hooks.run("after_error_response")
             cherrypy.response.finalize()
         except cherrypy.HTTPRedirect, inst:
-            inst.set_response()
+            ep = self.error_page.get(inst.status, '')
+            if ep and callable(ep):
+                ep(inst)
+            else:
+                inst.set_response()
             cherrypy.response.finalize()
 
 

cherrypy/test/test_core.py

         def as_refyield(self):
             for chunk in self.as_yield():
                 yield chunk
-
-
+    
+    
+    def page401(e):
+        cherrypy.response.status = e.status
+        cherrypy._cperror.clean_headers(e.status)
+        cherrypy.response.body = "Well, I'm very sorry but you haven't paid!"
+    
+    
     class Error(Test):
         
         _cp_config = {'tools.log_tracebacks.on': True,
                       }
         
-        def custom(self):
-            raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
-        custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
+        def custom(self, err='404'):
+            raise cherrypy.HTTPError(int(err), "No, <b>really</b>, not found!")
+        custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"),
+                             'error_page.401': page401,
+                             }
         
         def noexist(self):
             raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
         self.assertStatus(404)
         self.assertBody("Hello, world\r\n" + (" " * 499))
         
+        # Test custom error page.
+        self.getPage("/error/custom?err=401")
+        self.assertStatus(401)
+        self.assertBody("Well, I'm very sorry but you haven't paid!")
+        
         # Test error in custom error page (ticket #305).
         # Note that the message is escaped for HTML (ticket #310).
         self.getPage("/error/noexist")
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.