Anonymous avatar Anonymous committed 66ad903

ticket:288 changes merged into trunk, deleted httperrors branch, added assertErrorPage to CPWebCase

Comments (0)

Files changed (10)

cherrypy/_cpcgifs.py

             cgi.FieldStorage.__init__(self, *args, **kwds)
         except ValueError, ex:
             if str(ex) == 'Maximum content length exceeded':
-                raise cherrypy.HTTPStatusError(status=413)
+                raise cherrypy.HTTPError(status=413)
             else:
                 raise ex
 

cherrypy/_cperror.py

 
 _missing = object()
 
-class HTTPStatusError(Error):
-    """Exception raised when the client has made an error in its request."""
+class HTTPError(Error):
+    """ Exception raised when the client has made an error in its request.
+        This exception will automatically set the response status and body.
+        
+        A custom body can be pased to the init method in place of the standard error page.
+    """
     
-    def __init__(self, status=400, message=_missing):
+    def __init__(self, status=500, body=_missing):
         self.status = status = int(status)
         if status < 400 or status > 599:
             raise ValueError("status must be between 400 and 599.")
         
         # these 4 lines might dissapear
         import cherrypy
-        cherrypy.response.status = status
-        if message is not _missing:
-            cherrypy.response.body=message
+        self.statusString = cherrypy._cputil.getErrorStatusAndPage(status)[0]
+        cherrypy.response.status = self.statusString
 
-        self.message = message
+        if body is _missing:
+            # because the init method is called before the exception is raised
+            # it is impossible to embed the traceback in the error page at this point.
+            # We use a generator so that the error page is generated at a later point (
+            # after the exception is raised).
+            cherrypy.response.body = self.pageGenerator()
+        else:
+            cherrypy.response.body = body
     
-    def getArgs(self):
-        return (self.status, self.message)
+    def __str__(self):
+        return self.statusString
 
+    def pageGenerator(self):
+        import cherrypy
+        yield cherrypy._cputil.getErrorStatusAndPage(self.status)[1]
 
-class NotFound(HTTPStatusError):
+class NotFound(HTTPError):
     """ Happens when a URL couldn't be mapped to any class.method """
     
     def __init__(self, path):
         self.args = (path,)
-        HTTPStatusError.__init__(self, 404)
+        HTTPError.__init__(self, 404)

cherrypy/_cphttptools.py

 from urlparse import urlparse
 
 import cherrypy
-from cherrypy import _cputil, _cpcgifs, _cperror, _cpwsgiserver
+from cherrypy import _cputil, _cpcgifs, _cpwsgiserver, _cperror
 from cherrypy.lib import cptools
 
 
             finally:
                 applyFilters('onEndResource')
         except:
-            # This includes HTTPStatusError and NotFound
+            # This includes HTTPError and NotFound
             handleError(sys.exc_info())
     
     def processRequestHeaders(self):
                                       keep_blank_values=1)
         except _cpwsgiserver.MaxSizeExceeded:
             # Post data is too big
-            raise _cperror.HTTPStatusError(413)
+            raise _cperror.HTTPError(413)
         
         if forms.file:
             # request body was a content-type other than form params.
     try:
         applyFilters('beforeErrorResponse')
         
-        # _cpOnError and _cpOnHTTPError will probably change cherrypy.response.body.
+        # _cpOnError will probably change cherrypy.response.body.
         # They may also change the headerMap, etc.
-        if sys.exc_info()[0] is cherrypy.HTTPStatusError:
-            _cputil.getSpecialAttribute('_cpOnHTTPError')()
-        else:
-            _cputil.getSpecialAttribute('_cpOnError')()
+        _cputil.getSpecialAttribute('_cpOnError')()
         
         finalize()
         

cherrypy/_cputil.py

 #import os.path
 
 import cherrypy
-from cherrypy.lib import httperrors
-
 
 class EmptyClass:
     """ An empty class """
         f.write(s + '\n')
         f.close()
 
-def _cpOnHTTPError():
-    """ Default _cpOnHTTPError method """
-    status, customMessage = sys.exc_info()[1].getArgs()
-   
-    # get the error page
-    page = httperrors.getErrorPage(status, customMessage = customMessage)
-    cherrypy.response.status, cherrypy.response.body = page
+def _HTTPErrorTemplate(errorString, message, traceback, version):
+    subTuple = (errorString, errorString, message, traceback, cherrypy.__version__)
     
-    if cherrypy.response.headerMap.has_key('Content-Encoding'):
-        del cherrypy.response.headerMap['Content-Encoding']
+    return '''<?xml version="1.0" encoding="UTF-8"?>
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+    <html>
+    <head>
+        <title>%s</title>
+        <style type="text/css">
+        #poweredBy {
+            margin-top: 20px;
+            border-top: 2px solid black;
+            font-style: italic;
+        }
+
+        #traceback {
+            color: red;
+        }
+        </style>
+    </head>
+        <body>
+            <h2>%s</h2>
+            <p>%s</p>
+            <pre id="traceback">%s</pre>
+        <div id="poweredBy">
+        <span>Powered by <a href="http://www.cherrypy.org">Cherrypy %s</a></span>
+        </div>
+        </body>
+    </html>
+    ''' % subTuple
+
+import BaseHTTPServer
+_HTTPResponses = BaseHTTPServer.BaseHTTPRequestHandler.responses
+
+def getErrorStatusAndPage(status, traceback = None):
+    statusString, message = _HTTPResponses[status]
+    statusString = '%d %s' % (status, statusString)
+    
+    if traceback is None:
+        traceback = ''
+        # get the traceback from formatExc
+        developmentMode = (cherrypy.config.get('server.environment') == 'development')
+        if cherrypy.config.get('server.showTracebacks') or developmentMode:
+            traceback = formatExc()
+    
+    page = _HTTPErrorTemplate(statusString, message, traceback, cherrypy.__version__)
+    
+    return statusString, page
 
 def formatExc(exc=None):
     """formatExc(exc=None) -> exc (or sys.exc_info), formatted."""
     if exc is None:
         exc = sys.exc_info()
+
+    if exc == (None, None, None):
+        return ""
+    return "".join(traceback.format_exception(*exc))
+
+def getErrorStatusAndPage(status, traceback = None):
+    statusString, message = _HTTPResponses[status]
+    statusString = '%d %s' % (status, statusString)
+    
+    if traceback is None:
+        traceback = ''
+        # get the traceback from formatExc
+        developmentMode = (cherrypy.config.get('server.environment') == 'development')
+        if cherrypy.config.get('server.showTracebacks') or developmentMode:
+            traceback = formatExc()
+    
+    page = _HTTPErrorTemplate(statusString, message, traceback, cherrypy.__version__)
+    
+    return statusString, page
+
+    """formatExc(exc=None) -> exc (or sys.exc_info), formatted."""
+    if exc is None:
+        exc = sys.exc_info()
+    
+    if exc == (None, None, None):
+        return ""
+
     return "".join(traceback.format_exception(*exc))
 
 def _cpOnError():
     """ Default _cpOnError method """
-    developmentMode = cherrypy.config.get('server.environment') == 'development'
-    httpErrors      = cherrypy.config.get('server.httpErrors')
-    showTracebacks  = cherrypy.config.get('server.showTracebacks')
     
     logTracebacks  = cherrypy.config.get('server.logTracebacks', True)
     if logTracebacks:
     
     response = cherrypy.response
     
-    if not developmentMode and httpErrors:
-        # if it isn't development mode and http errors are turned on
-        # set the response status and render the body
-        if response.status == 404:
-            response.status, response.body = httperrors.getErrorPage(404)
-        else:
-            response.status, response.body = httperrors.getErrorPage(500)
-    elif developmentMode or showTracebacks:
-        # if it is development mode or tracebacks are turned on
-        # return the traceback as plaintext
-        response.body = [formatExc()]
-        response.headerMap['Content-Type'] = 'text/plain'
+    if isinstance(sys.exc_info()[1], cherrypy.HTTPError):
+        # status, body already set
+        pass
     else:
-        # If it is production mode but http errors are disabled then
-        # Display a simple, generic error message
-        response.body = "Unrecoverable error in the server"
-        response.headerMap['Content-Type'] = 'text/plain'
+        response.status, response.body = getErrorStatusAndPage(500)
     
     if cherrypy.response.headerMap.has_key('Content-Encoding'):
         del cherrypy.response.headerMap['Content-Encoding']

cherrypy/lib/cptools.py

     if result == []:
         cherrypy.response.headerMap['Content-Range'] = "bytes */%s" % content_length
         message = "Invalid Range (first-byte-pos greater than Content-Length)"
-        raise cherrypy.HTTPStatusError(416, message)
+        raise cherrypy.HTTPError(416, message)
     
     return result
 

cherrypy/lib/httperrors.py

-# this file contains built in error pages
-
-# this is code borrowed from python2.4's string module
-####################################################################
-import re as _re
-
-class _multimap:
-    """Helper class for combining multiple mappings.
-
-    Used by .{safe_,}substitute() to combine the mapping and keyword
-    arguments.
-    """
-    def __init__(self, primary, secondary):
-        self._primary = primary
-        self._secondary = secondary
-
-    def __getitem__(self, key):
-        try:
-            return self._primary[key]
-        except KeyError:
-            return self._secondary[key]
-
-
-class _TemplateMetaclass(type):
-    pattern = r"""
-    %(delim)s(?:
-      (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters
-      (?P<named>%(id)s)      |   # delimiter and a Python identifier
-      {(?P<braced>%(id)s)}   |   # delimiter and a braced identifier
-      (?P<invalid>)              # Other ill-formed delimiter exprs
-    )
-    """
-
-    def __init__(cls, name, bases, dct):
-        super(_TemplateMetaclass, cls).__init__(name, bases, dct)
-        if 'pattern' in dct:
-            pattern = cls.pattern
-        else:
-            pattern = _TemplateMetaclass.pattern % {
-                'delim' : _re.escape(cls.delimiter),
-                'id'    : cls.idpattern,
-                }
-        cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE)
-
-
-class Template:
-    """A string class for supporting $-substitutions."""
-    __metaclass__ = _TemplateMetaclass
-
-    delimiter = '$'
-    idpattern = r'[_a-z][_a-z0-9]*'
-
-    def __init__(self, template):
-        self.template = template
-
-    # Search for $$, $identifier, ${identifier}, and any bare $'s
-
-    def _invalid(self, mo):
-        i = mo.start('invalid')
-        lines = self.template[:i].splitlines(True)
-        if not lines:
-            colno = 1
-            lineno = 1
-        else:
-            colno = i - len(''.join(lines[:-1]))
-            lineno = len(lines)
-        raise ValueError('Invalid placeholder in string: line %d, col %d' %
-                         (lineno, colno))
-
-    def substitute(self, *args, **kws):
-        if len(args) > 1:
-            raise TypeError('Too many positional arguments')
-        if not args:
-            mapping = kws
-        elif kws:
-            mapping = _multimap(kws, args[0])
-        else:
-            mapping = args[0]
-        # Helper function for .sub()
-        def convert(mo):
-            # Check the most common path first.
-            named = mo.group('named') or mo.group('braced')
-            if named is not None:
-                val = mapping[named]
-                # We use this idiom instead of str() because the latter will
-                # fail if val is a Unicode containing non-ASCII characters.
-                return '%s' % val
-            if mo.group('escaped') is not None:
-                return self.delimiter
-            if mo.group('invalid') is not None:
-                self._invalid(mo)
-            raise ValueError('Unrecognized named group in pattern',
-                             self.pattern)
-        return self.pattern.sub(convert, self.template)
-
-    def safe_substitute(self, *args, **kws):
-        if len(args) > 1:
-            raise TypeError('Too many positional arguments')
-        if not args:
-            mapping = kws
-        elif kws:
-            mapping = _multimap(kws, args[0])
-        else:
-            mapping = args[0]
-        # Helper function for .sub()
-        def convert(mo):
-            named = mo.group('named')
-            if named is not None:
-                try:
-                    # We use this idiom instead of str() because the latter
-                    # will fail if val is a Unicode containing non-ASCII
-                    return '%s' % mapping[named]
-                except KeyError:
-                    return self.delimiter + named
-            braced = mo.group('braced')
-            if braced is not None:
-                try:
-                    return '%s' % mapping[braced]
-                except KeyError:
-                    return self.delimiter + '{' + braced + '}'
-            if mo.group('escaped') is not None:
-                return self.delimiter
-            if mo.group('invalid') is not None:
-                return self.delimiter
-            raise ValueError('Unrecognized named group in pattern',
-                             self.pattern)
-        return self.pattern.sub(convert, self.template)
-
-_defaultStyle ='''
-    <style type="text/css">
-    .poweredBy {
-        margin-top: 20px;
-        border-top: 2px solid black;
-    }
-
-    #traceback {
-        color: red;
-    }
-    </style>
-'''
-
-_defaultTemplate = Template('''<?xml version="1.0" encoding="$encoding"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html>
-<head>
-    <title>$errorString</title>
-    $style
-</head>
-    <body>
-        <h2>$errorString</h2>
-        <p>$message</p>
-        <pre id="traceback">$traceback</pre>
-    <div class="poweredBy">
-    <span>Powered by <a href="http://www.cherrypy.org">CherryPy $version</a></span>
-    </div>
-    </body>
-</html>
-''')
-
-import cherrypy
-
-import BaseHTTPServer
-_httpResponses = BaseHTTPServer.BaseHTTPRequestHandler.responses
-
-_templateDefaults = {
-        'encoding' : 'UTF-8',
-        'message'  : 'There was an error',
-        'traceback' : '',
-        'referer' : '',
-        'requestPath' : ''
-    }
-
-def getErrorPage(status, customTrace = None, customMessage = None):
-    statusString, message = _httpResponses[status]
-    if customMessage is not None:
-        message = customMessage
-
-    statusString = '%d %s' % (status, statusString)
-    
-    templateData = _templateDefaults.copy()
-    
-    templateData['errorString'] = statusString
-    templateData['message'] = message
-    
-    templateData['requestPath'] = cherrypy.request.path
-    templateData['version'] = cherrypy.__version__
-
-    style = cherrypy.config.get('httperror.style', _defaultStyle)
-    templateData['style'] = style
-    
-    if customTrace:
-        templateData['traceback'] = customTrace
-    else:
-        defaultOn = (cherrypy.config.get('server.environment') == 'development')
-        if cherrypy.config.get('server.showTracebacks', defaultOn):
-            templateData['traceback'] = cherrypy._cputil.formatExc()
-    page = _defaultTemplate.safe_substitute(templateData)
-    return statusString, page

cherrypy/test/helper.py

 import cherrypy
 import webtest
 import types
+import re
+
 for _x in dir(cherrypy):
     y = getattr(cherrypy, _x)
     if isinstance(y, types.ClassType) and issubclass(y, cherrypy.Error):
             self._getRequest(url, headers, method, body)
         else:
             webtest.WebCase.getPage(self, url, headers, method, body)
+ 
+    def assertErrorPage(self, errorCode, pattern = ''):
+        """ Compare the response body with a built in error page.
+            The function will optionally look for the regexp pattern, 
+            within the exception embedded in the error page.
+        """
+
+        from cherrypy._cputil import getErrorStatusAndPage
+        page = getErrorStatusAndPage(errorCode, '')[1]
+
+        # escape the question marks
+        page = page.replace('?', r'\?')
+        
+        # re to find the traceback in the page
+        traceRe = re.compile('(<pre id="traceback">)(</pre>)')
+
+        # stick the pattern in the page so we can match everythign
+        # at once
+        page = traceRe.sub( '\g<1>.*%s.*\g<2>' % pattern, page)
+        
+        # check if there is no exception
+        if pattern and traceRe.search(self.body):
+            msg = 'No match for %s in body' % `pattern`
+            self._handlewebError(msg)
+        else:
+            if not re.search(page, self.body, re.DOTALL):
+                msg = 'Error page does not match'
+                self._handlewebError(msg)
 
 CPTestLoader = webtest.ReloadingTestLoader()
 CPTestRunner = webtest.TerseTestRunner(verbosity=2)

cherrypy/test/test_core.py

     def testErrorHandling(self):
         self.getPage("/error/missing")
         self.assertStatus("404 Not Found")
-        self.assertInBody("NotFound")
+        self.assertErrorPage(404)
         
         ignore = helper.webtest.ignored_exceptions
         ignore.append(ValueError)
         try:
-            valerr = r'\n    raise ValueError\(\)\nValueError\n$'
+            valerr = r'\n    raise ValueError\(\)\nValueError\n'
             self.getPage("/error/page_method")
-            self.assertMatchesBody(valerr)
-            
+            self.assertErrorPage(500, valerr)
+
             import cherrypy
             proto = cherrypy.config.get("server.protocolVersion", "HTTP/1.0")
             if proto == "HTTP/1.1":

cherrypy/test/test_gzip_filter.py

 cherrypy.config.update({
         'server.logToScreen': False,
         'server.environment': 'production',
-        'server.httpErrors': False,
         'server.showTracebacks': True,
         'gzipFilter.on': True,
 })
                 self.assertMatchesBody(r"Unrecoverable error in the server.$")
             else:
                 self.assertNoHeader('Content-Encoding')
-                self.assertMatchesBody(r"IndexError\n$")
+                self.assertStatus('500 Internal error')
+                self.assertErrorPage(500, "IndexError\n")
         finally:
             helper.webtest.ignored_exceptions.pop()
 

cherrypy/tutorial/tut10_http_errors.py

         <html><body>
             <a href="toggleTracebacks">Toggle tracebacks %s</a><br/><br/>
             <a href="/doesNotExist">Click me i'm a broken link!</a>
+            <br/>
+            <a href="/customMessage">Use a custom error message</a>
             <br/><br/>
             These errors are explicitly raised by the application.
             <a href="/error?code=400">400</a>
     def error(self, code):
         # raise an error based on the get query
         code = int(code)
-        raise cherrypy.HTTPStatusError(status = code)
+        raise cherrypy.HTTPError(status = code)
     error.exposed = True
 
+    def customMessage(self):
+        raise cherrypy.HTTPError(500, "Plain text message")
+    customMessage.exposed = True
+
 cherrypy.root = HTTPErrorDemo()
 
 if __name__ == '__main__':
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.