Commits

Robert Brewer  committed e5716ae

Lots of changes to error and log handling:

1. Removed RequestHandled and InternalError.
2. Error response is now overridable by replacing request.error_response. Tools should do this in setup().
3. New request.log_access attribute.
4. Moved response.handleError to request.handle_error.
5. Logging of tracebacks and request headers are now tools.
6. New ErrorRedirect tool class.
7. Tools may now be anonymous (not necessary to be placed in tools module globals).
8. Continued the move to lower_with_underscores.

  • Participants
  • Parent commits 48d719a
  • Branches cherrypy

Comments (0)

Files changed (18)

     """Syntactic sugar for writing to the (error) log."""
     # Load _cputil lazily to avoid circular references, and
     # to allow profiler and coverage tools to work on it.
-    import _cputil
+    import _cputil, _cperror
     logfunc = _cputil.get_special_attribute('_cp_log_message')
     
     if traceback:
-        msg += _cputil.formatExc()
+        msg += _cperror.format_exc()
     
     logfunc(msg, context, severity)
 
 """Error classes for CherryPy."""
 
+import cgi
+import sys
+import traceback
 import urllib
+import urlparse
+
+from cherrypy.lib import httptools
+
 
 class Error(Exception):
     pass
     """ Happens when a config value can't be parsed, or is otherwise illegal. """
     pass
 
-class RequestHandled(Exception):
-    """Exception raised when no further request handling should occur."""
-    pass
-
 class InternalRedirect(Exception):
     """Exception raised when processing should be handled by a different path.
     
     
     def __init__(self, path, params=None):
         import cherrypy
-        import cgi
         request = cherrypy.request
         
         # Set a 'path' member attribute so that code which traps this
                 request.params = pm
             else:
                 request.query_string = urllib.urlencode(params)
-                request.queryString = request.query_string
                 request.params = params.copy()
         
         Exception.__init__(self, path, params)
     """
     
     def __init__(self, urls, status=None):
-        import urlparse
         import cherrypy
         
         if isinstance(urls, basestring):
         Error.__init__(self, status, message)
     
     def set_response(self):
+        """Set cherrypy.response status, headers, and body."""
         import cherrypy
-        handler = cherrypy._cputil.get_special_attribute("_cp_on_http_error")
-        handler(self.status, self.message)
+        
+        response = cherrypy.response
+        
+        # Remove headers which applied to the original content,
+        # but do not apply to the error page.
+        for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
+                    "Vary", "Content-Encoding", "Content-Length", "Expires",
+                    "Content-Location", "Content-MD5", "Last-Modified"]:
+            if response.headers.has_key(key):
+                del response.headers[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 response.headers.has_key("Content-Range"):
+                del response.headers["Content-Range"]
+        
+        # In all cases, finalize will be called after this method,
+        # so don't bother cleaning up response values here.
+        response.status = self.status
+        tb = None
+        if cherrypy.config.get('server.show_tracebacks', False):
+            tb = format_exc()
+        content = get_error_page(self.status, traceback=tb,
+                                 message=self.message)
+        response.body = content
+        response.headers['Content-Length'] = len(content)
+        response.headers['Content-Type'] = "text/html"
+        
+        be_ie_unfriendly(self.status)
 
 
 class NotFound(HTTPError):
         HTTPError.__init__(self, 404, "The path %s was not found." % repr(path))
 
 
-class InternalError(HTTPError):
-    """ Error that should never happen """
+_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
+    <title>%(status)s</title>
+    <style type="text/css">
+    #powered_by {
+        margin-top: 20px;
+        border-top: 2px solid black;
+        font-style: italic;
+    }
+
+    #traceback {
+        color: red;
+    }
+    </style>
+</head>
+    <body>
+        <h2>%(status)s</h2>
+        <p>%(message)s</p>
+        <pre id="traceback">%(traceback)s</pre>
+    <div id="powered_by">
+    <span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
+    </div>
+    </body>
+</html>
+'''
+
+def get_error_page(status, **kwargs):
+    """Return an HTML page, containing a pretty error response.
     
-    def __init__(self, message=None):
-        HTTPError.__init__(self, 500, message)
+    status should be an int or a str.
+    kwargs will be interpolated into the page template.
+    """
+    import cherrypy
+    
+    try:
+        code, reason, message = httptools.validStatus(status)
+    except ValueError, x:
+        raise cherrypy.HTTPError(500, x.args[0])
+    
+    # We can't use setdefault here, because some
+    # callers send None for kwarg values.
+    if kwargs.get('status') is None:
+        kwargs['status'] = "%s %s" % (code, reason)
+    if kwargs.get('message') is None:
+        kwargs['message'] = message
+    if kwargs.get('traceback') is None:
+        kwargs['traceback'] = ''
+    if kwargs.get('version') is None:
+        kwargs['version'] = cherrypy.__version__
+    for k, v in kwargs.iteritems():
+        if v is None:
+            kwargs[k] = ""
+        else:
+            kwargs[k] = cgi.escape(kwargs[k])
+    
+    template = _HTTPErrorTemplate
+    error_page_file = cherrypy.config.get('error_page.%s' % code, '')
+    if error_page_file:
+        try:
+            template = file(error_page_file, 'rb').read()
+        except:
+            m = kwargs['message']
+            if m:
+                m += "<br />"
+            m += ("In addition, the custom error page "
+                  "failed:\n<br />%s" % (sys.exc_info()[1]))
+            kwargs['message'] = m
+    
+    return template % kwargs
 
+
+_ie_friendly_error_sizes = {
+    400: 512, 403: 256, 404: 512, 405: 256,
+    406: 512, 408: 512, 409: 512, 410: 256,
+    500: 512, 501: 512, 505: 512,
+    }
+
+
+def be_ie_unfriendly(status):
+    import cherrypy
+    response = cherrypy.response
+    
+    # For some statuses, Internet Explorer 5+ shows "friendly error
+    # messages" instead of our response.body if the body is smaller
+    # than a given size. Fix this by returning a body over that size
+    # (by adding whitespace).
+    # See http://support.microsoft.com/kb/q218155/
+    s = _ie_friendly_error_sizes.get(status, 0)
+    if s:
+        s += 1
+        # Since we are issuing an HTTP error status, we assume that
+        # the entity is short, and we should just collapse it.
+        content = response.collapse_body()
+        l = len(content)
+        if l and l < s:
+            # IN ADDITION: the response must be written to IE
+            # in one chunk or it will still get replaced! Bah.
+            content = content + (" " * (s - l))
+        response.body = content
+        response.headers['Content-Length'] = len(content)
+
+
+def format_exc(exc=None):
+    """format_exc(exc=None) -> exc (or sys.exc_info if None), formatted."""
+    if exc is None:
+        exc = sys.exc_info()
+    if exc == (None, None, None):
+        return ""
+    return "".join(traceback.format_exception(*exc))
+
+def bare_error(extrabody=None):
+    """Produce status, headers, body for a critical error.
+    
+    Returns a triple without calling any other questionable functions,
+    so it should be as error-free as possible. Call it from an HTTP server
+    if you get errors outside of the request.
+    
+    If extrabody is None, a friendly but rather unhelpful error message
+    is set in the body. If extrabody is a string, it will be appended
+    as-is to the body.
+    """
+    
+    # The whole point of this function is to be a last line-of-defense
+    # in handling errors. That is, it must not raise any errors itself;
+    # it cannot be allowed to fail. Therefore, don't add to it!
+    # In particular, don't call any other CP functions.
+    
+    body = "Unrecoverable error in the server."
+    if extrabody is not None:
+        body += "\n" + extrabody
+    
+    return ("500 Internal Server Error",
+            [('Content-Type', 'text/plain'),
+             ('Content-Length', str(len(body)))],
+            [body])
+

File _cprequest.py

 import types
 
 import cherrypy
-from cherrypy import _cputil, _cpcgifs, tools
+from cherrypy import _cperror, _cputil, _cpcgifs, tools
 from cherrypy.lib import cptools, httptools
 
 
         remote_host should be string of the client's IP address.
         scheme should be a string, either "http" or "https".
         """
-        self.remote_addr  = remote_addr
-        self.remote_port  = remote_port
-        self.remote_host  = remote_host
+        self.remote_addr = remote_addr
+        self.remote_port = remote_port
+        self.remote_host = remote_host
         
         self.scheme = scheme
         self.closed = False
             self.hooks.run('on_end_request')
             cherrypy.serving.__dict__.clear()
     
-    def run(self, requestLine, headers, rfile):
+    def run(self, request_line, headers, rfile):
         """Process the Request.
         
-        requestLine should be of the form "GET /path HTTP/1.0".
+        request_line should be of the form "GET /path HTTP/1.0".
         headers should be a list of (name, value) tuples.
         rfile should be a file-like object containing the HTTP request
             entity.
         attributes to build the outbound stream.
         
         """
-        self.requestLine = requestLine.strip()
+        self.log_access = _cputil.log_access
+        self.error_response = cherrypy.HTTPError(500).set_response
+        
+        self.request_line = request_line.strip()
         self.header_list = list(headers)
         self.rfile = rfile
-        
         self.headers = httptools.HeaderMap()
         self.simple_cookie = Cookie.SimpleCookie()
         
             # HEAD requests MUST NOT return a message-body in the response.
             cherrypy.response.body = []
         
-        _cputil.get_special_attribute("_cp_log_access")()
+        self.log_access()
         
         return cherrypy.response
     
     def _run(self):
+        conf = cherrypy.config.get
         
         try:
             # This has to be done very early in the request process,
             # because request.object_path is used for config lookups
             # right away.
-            self.processRequestLine()
-            self.dispatch = cherrypy.config.get("dispatch") or _cputil.dispatch
+            self.process_request_line()
+            self.dispatch = conf("dispatch") or _cputil.dispatch
             self.hooks.setup()
             
             try:
                 self.hooks.run('on_start_resource')
                 
                 try:
-                    self.processHeaders()
+                    self.process_headers()
                     
                     self.hooks.run('before_request_body')
-                    if self.processRequestBody:
-                        self.processBody()
+                    if self.process_request_body:
+                        self.process_body()
                     
                     # Loop to allow for InternalRedirect.
                     while True:
                     
                     self.hooks.run('before_finalize')
                     cherrypy.response.finalize()
-                except cherrypy.RequestHandled:
-                    pass
                 except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
                     # For an HTTPRedirect or HTTPError (including NotFound),
                     # we don't go through the regular mechanism:
         except (KeyboardInterrupt, SystemExit):
             raise
         except:
-            if cherrypy.config.get("server.throw_errors", False):
+            if conf("server.throw_errors", False):
                 raise
-            cherrypy.response.handleError(sys.exc_info())
+            self.handle_error(sys.exc_info())
     
-    def processRequestLine(self):
-        rl = self.requestLine
-        method, path, qs, proto = httptools.parseRequestLine(rl)
+    def process_request_line(self):
+        """Parse the first line (e.g. "GET /path HTTP/1.1") of the request."""
+        rl = self.request_line
+        method, path, qs, proto = httptools.parse_request_line(rl)
         if path == "*":
             path = "global"
         
         self.method = method
-        self.processRequestBody = method in ("POST", "PUT")
+        self.process_request_body = method in ("POST", "PUT")
         
         self.path = path
         self.query_string = qs
         server_v = httptools.Version.from_http(server_v)
         cherrypy.response.version = min(self.version, server_v)
     
-    def processHeaders(self):
+    def process_headers(self):
         self.params = httptools.parseQueryString(self.query_string)
         
         # Process the headers into self.headers
     browser_url = property(_get_browser_url,
                           doc="The URL as entered in a browser (read-only).")
     
-    def processBody(self):
+    def process_body(self):
+        """Convert request.rfile into request.params (or request.body)."""
         # Create a copy of headers with lowercase keys because
         # FieldStorage doesn't work otherwise
         lowerHeaderMap = {}
             self.body = forms.file
         else:
             self.params.update(httptools.paramsFromCGIForm(forms))
+    
+    def handle_error(self, exc):
+        response = cherrypy.response
+        try:
+            self.hooks.run("before_error_response")
+            if self.error_response:
+                self.error_response()
+            self.hooks.run("after_error_response")
+            response.finalize()
+            return
+        except cherrypy.HTTPRedirect, inst:
+            try:
+                inst.set_response()
+                response.finalize()
+                return
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except:
+                # Fall through to the second error handler
+                pass
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except:
+            # Fall through to the second error handler
+            pass
+        
+        # Failure in error handler or finalize. Bypass them.
+        if cherrypy.config.get('server.show_tracebacks', False):
+            dbltrace = ("\n===First Error===\n\n%s"
+                        "\n\n===Second Error===\n\n%s\n\n")
+            body = dbltrace % (_cperror.format_exc(exc),
+                               _cperror.format_exc())
+        else:
+            body = ""
+        r = _cperror.bare_error(body)
+        response.status, response.header_list, response.body = r
 
 
 class Body(object):
             for line in cookie.split("\n"):
                 name, value = line.split(": ", 1)
                 self.header_list.append((name, value))
-    
-    dbltrace = "\n===First Error===\n\n%s\n\n===Second Error===\n\n%s\n\n"
-    
-    def handleError(self, exc):
-        """Set status, headers, and body when an unanticipated error occurs."""
-        try:
-            cherrypy.request.hooks.run('before_error_response')
-            
-            _cputil.get_special_attribute('_cp_on_error')()
-            self.finalize()
-            
-            cherrypy.request.hooks.run('after_error_response')
-            return
-        except cherrypy.HTTPRedirect, inst:
-            try:
-                inst.set_response()
-                self.finalize()
-                return
-            except (KeyboardInterrupt, SystemExit):
-                raise
-            except:
-                # Fall through to the second error handler
-                pass
-        except (KeyboardInterrupt, SystemExit):
-            raise
-        except:
-            # Fall through to the second error handler
-            pass
-        
-        # Failure in error hooks or finalize.
-        # Bypass them all.
-        if cherrypy.config.get('server.show_tracebacks', False):
-            body = self.dbltrace % (_cputil.formatExc(exc),
-                                    _cputil.formatExc())
-        else:
-            body = ""
-        self.setBareError(body)
-    
-    def setBareError(self, body=None):
-        self.status, self.header_list, self.body = _cputil.bareError(body)
-
 
 def get_special_attribute(name):
     """Return the special attribute. A special attribute is one that
-    applies to all of the children from where it is defined, such as
-    _cp_on_error."""
+    applies to all of the children from where it is defined."""
     
     # First, we look in the right-most object to see if this special
     # attribute is implemented. If not, then we try the previous object,
     return ""
 _cpGlobalHandler.exposed = True
 
+
 def logtime():
     now = datetime.datetime.now()
     month = httptools.monthname[now.month][:3].capitalize()
     return '%02d/%s/%04d:%02d:%02d:%02d' % (
         now.day, month, now.year, now.hour, now.minute, now.second)
 
-def _cp_log_access():
+def log_access():
     """Default method for logging access"""
+    request = cherrypy.request
     
     tmpl = '%(h)s %(l)s %(u)s [%(t)s] "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
-    s = tmpl % {'h': cherrypy.request.remote_host,
+    s = tmpl % {'h': request.remote_host,
                 'l': '-',
-                'u': getattr(cherrypy.request, "login", None) or "-",
+                'u': getattr(request, "login", None) or "-",
                 't': logtime(),
-                'r': cherrypy.request.requestLine,
+                'r': request.request_line,
                 's': cherrypy.response.status.split(" ", 1)[0],
                 'b': cherrypy.response.headers.get('Content-Length', '') or "-",
-                'f': cherrypy.request.headers.get('referer', ''),
-                'a': cherrypy.request.headers.get('user-agent', ''),
+                'f': request.headers.get('referer', ''),
+                'a': request.headers.get('user-agent', ''),
                 }
     
     if cherrypy.config.get('server.log_to_screen', True):
         f.write(s + '\n')
         f.close()
 
-
 _log_severity_levels = {0: "INFO", 1: "WARNING", 2: "ERROR"}
 
 def _cp_log_message(msg, context = '', severity = 0):
         f.write(s + '\n')
         f.close()
 
-
-_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html>
-<head>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
-    <title>%(status)s</title>
-    <style type="text/css">
-    #powered_by {
-        margin-top: 20px;
-        border-top: 2px solid black;
-        font-style: italic;
-    }
-
-    #traceback {
-        color: red;
-    }
-    </style>
-</head>
-    <body>
-        <h2>%(status)s</h2>
-        <p>%(message)s</p>
-        <pre id="traceback">%(traceback)s</pre>
-    <div id="powered_by">
-    <span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
-    </div>
-    </body>
-</html>
-'''
-
-def getErrorPage(status, **kwargs):
-    """Return an HTML page, containing a pretty error response.
-    
-    status should be an int or a str.
-    kwargs will be interpolated into the page template.
-    """
-    
-    try:
-        code, reason, message = httptools.validStatus(status)
-    except ValueError, x:
-        raise cherrypy.HTTPError(500, x.args[0])
-    
-    # We can't use setdefault here, because some
-    # callers send None for kwarg values.
-    if kwargs.get('status') is None:
-        kwargs['status'] = "%s %s" % (code, reason)
-    if kwargs.get('message') is None:
-        kwargs['message'] = message
-    if kwargs.get('traceback') is None:
-        kwargs['traceback'] = ''
-    if kwargs.get('version') is None:
-        kwargs['version'] = cherrypy.__version__
-    for k, v in kwargs.iteritems():
-        if v is None:
-            kwargs[k] = ""
-        else:
-            kwargs[k] = cgi.escape(kwargs[k])
-    
-    template = _HTTPErrorTemplate
-    error_page_file = cherrypy.config.get('error_page.%s' % code, '')
-    if error_page_file:
-        try:
-            template = file(error_page_file, 'rb').read()
-        except:
-            m = kwargs['message']
-            if m:
-                m += "<br />"
-            m += ("In addition, the custom error page "
-                  "failed:\n<br />%s" % (sys.exc_info()[1]))
-            kwargs['message'] = m
-    
-    return template % kwargs
-
-def _cp_on_error():
-    """ Default _cp_on_error method """
-    # Allow logging of only *unexpected* HTTPError's.
-    if (not cherrypy.config.get('server.log_tracebacks', True)
-        and cherrypy.config.get('server.log_unhandled_tracebacks', True)):
-        cherrypy.log(traceback=True)
-    cherrypy.HTTPError(500).set_response()
-
-def _cp_on_http_error(status, message):
-    """Default _cp_on_http_error method.
-    
-    status should be an int.
-    """
-    tb = formatExc()
-    logmsg = ""
-    
-    if cherrypy.config.get('server.log_tracebacks', True):
-        logmsg = tb
-    if cherrypy.config.get('server.log_request_headers', True):
-        h = ["  %s: %s" % (k, v) for k, v in cherrypy.request.header_list]
-        logmsg += 'Request Headers:\n' + '\n'.join(h)
-    if logmsg:
-        cherrypy.log(logmsg, "HTTP")
-    
-    if not cherrypy.config.get('server.show_tracebacks', False):
-        tb = None
-    
-    response = cherrypy.response
-    
-    # Remove headers which applied to the original content,
-    # but do not apply to the error page.
-    for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
-                "Vary", "Content-Encoding", "Content-Length", "Expires",
-                "Content-Location", "Content-MD5", "Last-Modified"]:
-        if response.headers.has_key(key):
-            del response.headers[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 response.headers.has_key("Content-Range"):
-            del response.headers["Content-Range"]
-    
-    # In all cases, finalize will be called after this method,
-    # so don't bother cleaning up response values here.
-    response.status = status
-    content = getErrorPage(status, traceback=tb, message=message)
-    response.body = content
-    response.headers['Content-Length'] = len(content)
-    response.headers['Content-Type'] = "text/html"
-    
-    be_ie_unfriendly(status)
-
-
-_ie_friendly_error_sizes = {
-    400: 512, 403: 256, 404: 512, 405: 256,
-    406: 512, 408: 512, 409: 512, 410: 256,
-    500: 512, 501: 512, 505: 512,
-    }
-
-
-def be_ie_unfriendly(status):
-    
-    response = cherrypy.response
-    
-    # For some statuses, Internet Explorer 5+ shows "friendly error
-    # messages" instead of our response.body if the body is smaller
-    # than a given size. Fix this by returning a body over that size
-    # (by adding whitespace).
-    # See http://support.microsoft.com/kb/q218155/
-    s = _ie_friendly_error_sizes.get(status, 0)
-    if s:
-        s += 1
-        # Since we are issuing an HTTP error status, we assume that
-        # the entity is short, and we should just collapse it.
-        content = response.collapse_body()
-        l = len(content)
-        if l and l < s:
-            # IN ADDITION: the response must be written to IE
-            # in one chunk or it will still get replaced! Bah.
-            content = content + (" " * (s - l))
-        response.body = content
-        response.headers['Content-Length'] = len(content)
-
-def lower_to_camel(s):
-    """Turns lowercase_with_underscore into camelCase."""
-    sp = s.split('_')
-    new_sp = []
-    for i, s in enumerate(sp):
-        if i != 0:
-            s = s[0].upper() + s[1:]
-        new_sp.append(s)
-    return ''.join(new_sp)
-
-def formatExc(exc=None):
-    """formatExc(exc=None) -> exc (or sys.exc_info if None), formatted."""
-    if exc is None:
-        exc = sys.exc_info()
-    
-    if exc == (None, None, None):
-        return ""
-    
-    page_handler_str = ""
-    args = list(getattr(exc[1], "args", []))
-    if args:
-        if len(args) > 1:
-            page_handler = args.pop()
-            page_handler_str = 'Page handler: %s\n' % repr(page_handler)
-            exc[1].args = tuple(args)
-    return page_handler_str + "".join(traceback.format_exception(*exc))
-
-def bareError(extrabody=None):
-    """Produce status, headers, body for a critical error.
-    
-    Returns a triple without calling any other questionable functions,
-    so it should be as error-free as possible. Call it from an HTTP server
-    if you get errors after Request() is done.
-    
-    If extrabody is None, a friendly but rather unhelpful error message
-    is set in the body. If extrabody is a string, it will be appended
-    as-is to the body.
-    """
-    
-    # The whole point of this function is to be a last line-of-defense
-    # in handling errors. That is, it must not raise any errors itself;
-    # it cannot be allowed to fail. Therefore, don't add to it!
-    # In particular, don't call any other CP functions.
-    
-    body = "Unrecoverable error in the server."
-    if extrabody is not None:
-        body += "\n" + extrabody
-    
-    return ("500 Internal Server Error",
-            [('Content-Type', 'text/plain'),
-             ('Content-Length', str(len(body)))],
-            [body])
-
 import sys
 import cherrypy
 from cherrypy import _cputil, _cpwsgiserver
+from cherrypy._cperror import format_exc, bare_error
 from cherrypy.lib import httptools
 
 
-def requestLine(environ):
+def request_line(environ):
     """Rebuild first line of the request (e.g. "GET /path HTTP/1.0")."""
     
     resource = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
         request.multithread = environ['wsgi.multithread']
         request.multiprocess = environ['wsgi.multiprocess']
         request.wsgi_environ = environ
-        response = request.run(requestLine(environ),
+        response = request.run(request_line(environ),
                                translate_headers(environ),
                                environ['wsgi.input'])
         s, h, b = response.status, response.header_list, response.body
     except:
         if cherrypy.config.get("server.throw_errors", False):
             raise
-        tb = _cputil.formatExc()
+        tb = format_exc()
         cherrypy.log(tb)
         if not cherrypy.config.get("server.show_tracebacks", False):
             tb = ""
-        s, h, b = _cputil.bareError(tb)
+        s, h, b = bare_error(tb)
         exc = sys.exc_info()
     
     try:
         except:
             cherrypy.log(traceback=True)
         request = None
-        s, h, b = _cputil.bareError()
-        # CherryPy test suite expects bareError body to be output,
+        s, h, b = bare_error()
+        # CherryPy test suite expects bare_error body to be output,
         # so don't call start_response (which, according to PEP 333,
         # may raise its own error at that point).
         for chunk in b:
     'server.socket_queue_size': 5,
     'server.protocol_version': 'HTTP/1.0',
     'server.log_to_screen': True,
-    'server.log_tracebacks': True,
     'server.log_file': '',
+    'tools.log_tracebacks.on': True,
     'server.reverse_dns': False,
     'server.thread_pool': 10,
     'server.environment': "development",
                   'server.environment',
                   'server.log_to_screen',
                   'server.log_file',
-                  'server.log_tracebacks',
-                  'server.log_request_headers',
                   'server.protocol_version',
                   'server.socket_host',
                   'server.socket_port',

File lib/cptools.py

 import cherrypy
 
 
-
 def decorate(func, decorator):
     """
     Return the decorated func. This will automatically copy all
     if prefix:
         cherrypy.request.object_path = prefix + "/" + cherrypy.request.object_path
 
+def log_traceback():
+    """Write the last error's traceback to the cherrypy error log."""
+    from cherrypy import _cperror
+    cherrypy.log(_cperror.format_exc(), "HTTP")
 
+def log_request_headers():
+    """Write the last error's traceback to the cherrypy error log."""
+    h = ["  %s: %s" % (k, v) for k, v in cherrypy.request.header_list]
+    cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
+
+def redirect(url=''):
+    """Raise cherrypy.HTTPRedirect to the given url."""
+    raise cherrypy.HTTPRedirect(url)

File lib/httptools.py

     
     return code, reason, message
 
-def parseRequestLine(requestLine):
-    """Return (method, path, querystring, protocol) from a requestLine."""
-    method, path, protocol = requestLine.split()
+def parse_request_line(request_line):
+    """Return (method, path, querystring, protocol) from a request_line."""
+    method, path, protocol = request_line.split()
     
     # path may be an abs_path (including "http://host.domain.tld");
     # Ignore scheme, location, and fragments (so config lookups work).

File lib/sessions.py

 
 
 # Users access sessions through cherrypy.session, but we want this
-#   to be thread-specific so we use a special wrapper that forwards
-#   calls to cherrypy.session to a thread-specific dictionary called
+#   to be thread-specific so we use a wrapper that forwards calls
+#   to cherrypy.session to a thread-specific dictionary called
 #   cherrypy.request._session.data
 class SessionWrapper:
     

File lib/wsgiapp.py

     class WSGIAppRequest(_cphttptools.Request):
         """A custom Request object for running a WSGI app within CP."""
        
-        def processBody(self):
+        def process_body(self):
             pass
         
         def main(self, *args, **kwargs):

File lib/xmlrpc.py

                                   encoding=encoding,
                                   allow_none=allow_none))
 
-def wrap_error():
+def on_error():
     body = str(sys.exc_info()[1])
     _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))
 

File test/benchmark.py

     def close(self):
         pass
     
-    def run(self, requestLine, headers, rfile):
+    def run(self, request_line, headers, rfile):
         cherrypy.response.status = "204 No Content"
         cherrypy.response.header_list = [("Content-Type", 'text/html'),
                                          ("Server", "Null CherryPy"),

File test/helper.py

         """
         
         # This will never contain a traceback:
-        page = cherrypy._cputil.getErrorPage(status, message=message)
+        page = cherrypy._cperror.get_error_page(status, message=message)
         
         # First, test the response body without checking the traceback.
         # Stick a match-all group (.*) in to grab the traceback.

File test/test_config.py

     def testUnrepr(self):
         err = ('WrongConfigValue: ("section: '
                "'global', option: 'server.environment', value: 'production'"
-               '''", 'UnknownType')''')
+               '''", 'UnknownType', ('production',))''')
         self.getPage("/env/wrong")
         self.assertErrorPage(500, pattern=err)
     

File test/test_core.py

     class Redirect(Test):
         
         class Error:
-            def _cp_on_error(self):
-                raise cherrypy.HTTPRedirect("/errpage")
+            _cp_tools = [tools.ErrorRedirect("/errpage")]
             
             def index(self):
                 raise NameError()
             # Since status must start with an int, this should error.
             cherrypy.response.status = "ZOO OK"
         
-        def log_unhandled(self):
-            raise ValueError()
-        
         def rethrow(self):
             """Test that an error raised here will be thrown out to the server."""
             raise ValueError()
         },
         '/error': {
             'server.log_file': log_file,
-            'server.log_tracebacks': True,
+            'tools.log_tracebacks.on': True,
         },
         '/error/page_streamed': {
             'stream_response': True,
         '/error/noexist': {
             'error_page.404': "nonexistent.html",
         },
-        '/error/log_unhandled': {
-            'server.log_tracebacks': False,
-            'server.log_unhandled_tracebacks': True,
-        },
         '/error/rethrow': {
             'server.throw_errors': True,
         },
             data = open(log_file, "rb").readlines()
             self.assertEqual(data[0][-41:], ' INFO Traceback (most recent call last):\n')
             self.assertEqual(data[-3], '    raise ValueError()\n')
-            
-            # Test that unhandled tracebacks get written to the error log
-            # if log_tracebacks is False but log_unhandled_tracebacks is True.
-            self.getPage("/error/log_unhandled")
-            self.assertInBody("raise ValueError()")
-            data = open(log_file, "rb").readlines()
-            self.assertEqual(data[-3], '    raise ValueError()\n')
-            # Each error should write only one traceback (9 lines each).
-            self.assertEqual(len(data), 18)
         finally:
             ignore.pop()
     
         self.assertStatus(200)
     
     def testFavicon(self):
-        # Calls to favicon.ico are special-cased in config.py.
+        # favicon.ico is served by staticfile by default (see config.py)
         icofilename = os.path.join(localDir, "../favicon.ico")
         icofile = open(icofilename, "rb")
         data = icofile.read()

File test/test_custom_filters.py

         def __init__(self):
             self.counter = 0
             self.ended = {}
+            self.name = "nadsat"
         
         def nadsat(self):
             def nadsat_it_up(body):

File test/test_virtualhost_filter.py

     cherrypy.root.mydom3 = VHost("Domain 3")
 
     cherrypy.config.update({
-            'server.logToScreen': False,
+            'server.log_to_screen': False,
             'server.environment': 'production',
             'tools.virtual_host.on': True,
             'tools.virtual_host.www.mydom2.com': '/mydom2',
     
     def setup(self):
         """Run tool.setup(conf) for each tool specified in current config."""
+        toolconf = tool_config()
+        
         g = globals()
-        for toolname, conf in tool_config().iteritems():
+        for toolname, conf in toolconf.iteritems():
             if conf.get("on", False):
                 del conf["on"]
-                g[toolname].setup(conf)
+                tool = g.get(toolname)
+                if tool:
+                    tool.setup(conf)
         
-        # Run _cp_tools setup functions
+        # Run _cp_tools setup functions. They should be run
+        # in order: first from root to leaf, then in the order
+        # given within each _cp_tools list. However, if the
+        # same tool is mentioned twice, the one farthest from
+        # the root should be used. This allows a leaf node to
+        # override parent nodes and order.
         mounted_app_roots = cherrypy.tree.mount_points.values()
         objectList = _cputil.get_object_trail()
         objectList.reverse()
+        
+        # Collect toolsets (up to the app root).
+        toolsets = []
         for objname, obj in objectList:
-            for tool in getattr(obj, "_cp_tools", []):
-                tool.setup()
+            toolset = getattr(obj, "_cp_tools", [])
+            if toolset:
+                toolsets.append(toolset)
             if obj in mounted_app_roots:
                 break
+        
+        # Now reverse (and de-dupe) the tool list, and call each setup
+        toolsets.reverse()
+        seen = {}
+        for toolset in toolsets:
+            for tool in toolset:
+                obj_id = id(tool)
+                if obj_id not in seen:
+                    seen[obj_id] = None
+                    tool.setup(toolconf.get(tool.name, {}))
     
     def run(self, point, *args, **kwargs):
         """Execute all registered callbacks for the given point."""
         """
         def deco(f):
             def wrapper(*a, **kw):
-                handled = self.callable(*args, **merged_config(self.name, kwargs))
+                self.callable(*args, **merged_config(self.name, kwargs))
                 return f(*a, **kw)
             return wrapper
         return deco
         cherrypy.request.hooks.attach(self.point, wrapper)
 
 
+class ErrorTool(Tool):
+    """Tool which is used to replace the default request.error_response."""
+    
+    def __init__(self, callable, name=None):
+        Tool.__init__(self, None, callable, name)
+    
+    def setup(self, conf=None):
+        """Hook this tool into cherrypy.request using the given conf.
+        
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        def wrapper():
+            self.callable(**conf)
+        cherrypy.request.error_response = wrapper
+
+
+
 #                              Builtin tools                              #
 
 from cherrypy.lib import cptools
 base_url = Tool('before_request_body', cptools.base_url)
 response_headers = Tool('before_finalize', cptools.response_headers)
 virtual_host = Tool('before_request_body', cptools.virtual_host)
+log_tracebacks = Tool('before_error_response', cptools.log_traceback)
+log_headers = Tool('before_error_response', cptools.log_request_headers)
+
+_redirect = cptools.redirect
+class ErrorRedirect(Tool):
+    """Tool to redirect on error."""
+    
+    def __init__(self, url):
+        Tool.__init__(self, None, _redirect, "ErrorRedirect")
+        self.url = url
+    
+    def setup(self, conf=None):
+        """Hook this tool into cherrypy.request using the given conf.
+        
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        c = {'url': self.url}
+        c.update(conf or {})
+        def wrapper():
+            self.callable(**c)
+        cherrypy.request.error_response = wrapper
+
 del cptools
 
 from cherrypy.lib import encodings
             dispatch = tools.xmlrpc.dispatch
         """
         request = cherrypy.request
-        request.hooks.attach('after_error_response', _xmlrpc.wrap_error)
+        request.error_response = _xmlrpc.on_error
         
         rpcparams, rpcmethod = _xmlrpc.process_body()
         path = _xmlrpc.patched_path(path, rpcmethod)
     def setup(self, conf=None):
         """Hook this tool into cherrypy.request using the given conf."""
         cherrypy.request.dispatch = self.dispatch
+        cherrypy.request.error_response = _xmlrpc.on_error
 xmlrpc = _XMLRPCTool()
 
-
 # These modules are themselves Tools
 from cherrypy.lib import caching