Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Changeset 682

Show
Ignore:
Timestamp:
09/27/05 02:54:29
Author:
fumanchu
Message:

Fix for ticket #317? More importantly, some error-handling and other cleanups:

1. _cphttptools.checkStatus moved to lib.cptools.validStatus. Illegal status codes now raise HTTPError(500) with custom messages.
2. _cputil.responseCodes moved into cptools.
3. _cpOnError logic *all* moved into HTTPError.set_response.
4. _cputil.getErrorStatusAndPage changed to getErrorPage.
5. Made _cputil._HTTPErrorTemplate into a string instead of a function, which the new getErrorPage interpolates with its own **kwargs.
6. HTTPError now properly takes a "message" arg instead of a "body" arg. Specify alternate body via errorPage.4xx config entry (overriding _HTTPErrorTemplate).
7. HTTPError now allows str statuses, to allow arbitrary reason-phrases.
8. assertErrorPage in helper.py updated to match core changes.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/_cperror.py

    r679 r682  
    172172 
    173173 
    174 _missing = object() 
    175  
    176174class HTTPError(Error): 
    177175    """ Exception used to return an HTTP error code to the client. 
    178176        This exception will automatically set the response status and body. 
    179177         
    180         A custom body can be pased to the init method in place of the 
    181         standard error page
     178        A custom message (a long description to display in the browser) 
     179        can be provided in place of the default
    182180    """ 
    183181     
    184     def __init__(self, status=500, body=_missing): 
     182    def __init__(self, status=500, message=None): 
    185183        self.status = status = int(status) 
    186184        if status < 400 or status > 599: 
    187185            raise ValueError("status must be between 400 and 599.") 
    188           
    189         self.body = body 
     186        self.message = message 
    190187     
    191188    def set_response(self): 
    192189        import cherrypy 
    193          
    194         # we now now have access to the traceback  
    195         statusString, defaultBody = cherrypy._cputil.getErrorStatusAndPage(self.status) 
    196          
    197         if self.body is _missing: 
    198             self.body = defaultBody 
    199             # try to look up a custom error page in the config map 
    200             # if there is no error page then use the pageGenerator 
    201              
    202             # The page generator is used because the init method is called  
    203             # before the exception is raised.  It is impossible to embed the 
    204             # traceback in the error page at this piont so we use the generator 
    205             # to render the error page at a later point 
    206              
    207             import cherrypy 
    208             # try and read the page from a file 
    209             # we use the default if the page can't be read 
    210             try: 
    211                 errorPageFile = cherrypy.config.get('errorPage.%s' % status, '') 
    212                 self.body = file(errorPageFile, 'r') 
    213             except: 
    214                 # we have alread set the body 
    215                 pass 
    216  
    217         cherrypy.response.status = statusString 
    218         cherrypy.response.body   = self.body 
     190        from cherrypy._cputil import getErrorPage, formatExc 
     191         
     192        tb = formatExc() 
     193        if cherrypy.config.get('server.logTracebacks', True): 
     194            cherrypy.log(tb) 
     195         
     196        defaultOn = (cherrypy.config.get('server.environment') == 'development') 
     197        if not cherrypy.config.get('server.showTracebacks', defaultOn): 
     198            tb = None 
     199         
     200        # In all cases, finalize will be called after this method, 
     201        # so don't bother cleaning up response values here. 
     202        cherrypy.response.status = self.status 
     203        cherrypy.response.body = getErrorPage(self.status, traceback=tb, 
     204                                              message=self.message) 
     205         
     206        if cherrypy.response.headerMap.has_key("Content-Encoding"): 
     207            del cherrypy.response.headerMap['Content-Encoding'] 
    219208     
    220209    def __str__(self): 
    221210        import cherrypy 
    222         return cherrypy._cputil.getErrorStatusAndPage(self.status)[0] 
     211        return "%s: %s" % (self.status, self.message or "") 
     212 
    223213 
    224214class NotFound(HTTPError): 
     
    227217    def __init__(self, path): 
    228218        self.args = (path,) 
    229         HTTPError.__init__(self, 404) 
    230  
    231     def __str__(self): 
    232         return self.args[0] 
     219        HTTPError.__init__(self, 404, "The path %s was not found." % repr(path)) 
  • trunk/cherrypy/_cphttptools.py

    r679 r682  
    276276                except cherrypy.RequestHandled: 
    277277                    pass 
    278                 except cherrypy.HTTPRedirect, inst: 
    279                     # For an HTTPRedirect, we don't go through the regular 
    280                     # mechanism: we return the redirect immediately 
     278                except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst: 
     279                    # For an HTTPRedirect or HTTPError (including NotFound), 
     280                    # we don't go through the regular mechanism: 
     281                    # we return the redirect or error page immediately 
    281282                    inst.set_response() 
    282283                    applyFilters('beforeFinalize') 
    283284                    finalize() 
    284                 except cherrypy.HTTPError, inst: 
    285                     # This includes NotFound 
    286                     inst.set_response() 
    287                     applyFilters('beforeFinalize') 
    288                     finalize() 
    289  
    290285            finally: 
    291286                applyFilters('onEndResource') 
    292287        except: 
    293             # This includes HTTPError and NotFound 
    294288            handleError(sys.exc_info()) 
    295289     
     
    425419 
    426420def handleError(exc): 
    427     """Set status, headers, and body when an error occurs.""" 
     421    """Set status, headers, and body when an unanticipated error occurs.""" 
    428422    try: 
    429423        applyFilters('beforeErrorResponse') 
     
    437431        applyFilters('afterErrorResponse') 
    438432        return 
    439     except cherrypy.HTTPRedirect, inst: 
     433    except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst: 
    440434        try: 
    441435            inst.set_response() 
     
    520514    return body 
    521515 
    522 def checkStatus(): 
    523     """Test/set cherrypy.response.status. Provide Reason-phrase if missing.""" 
    524     if not cherrypy.response.status: 
    525         cherrypy.response.status = "200 OK" 
    526     else: 
    527         status = str(cherrypy.response.status) 
    528         parts = status.split(" ", 1) 
    529         if len(parts) == 1: 
    530             # No reason supplied. 
    531             code, = parts 
    532             reason = None 
    533         else: 
    534             code, reason = parts 
    535             reason = reason.strip() 
    536          
    537         try: 
    538             code = int(code) 
    539             assert code >= 100 and code < 600 
    540         except (ValueError, AssertionError): 
    541             code = 500 
    542             reason = None 
    543          
    544         if reason is None: 
    545             try: 
    546                 reason = _cputil.responseCodes[code][0] 
    547             except (KeyError, IndexError): 
    548                 reason = "" 
    549          
    550         cherrypy.response.status = "%s %s" % (code, reason) 
    551     return cherrypy.response.status 
    552  
    553516 
    554517general_header_fields = ["Cache-Control", "Connection", "Date", "Pragma", 
     
    580543    """Transform headerMap (and cookies) into cherrypy.response.headers.""" 
    581544     
    582     checkStatus() 
    583      
    584545    response = cherrypy.response 
     546     
     547    code, reason, _ = cptools.validStatus(response.status) 
     548    response.status = "%s %s" % (code, reason) 
     549     
    585550    if response.body is None: 
    586551        response.body = [] 
  • trunk/cherrypy/_cputil.py

    r681 r682  
    3434import traceback 
    3535import time 
    36 #import os.path 
    37  
    38 from BaseHTTPServer import BaseHTTPRequestHandler 
    39 responseCodes = BaseHTTPRequestHandler.responses 
    4036 
    4137import cherrypy 
     38from cherrypy.lib import cptools 
     39 
    4240 
    4341class EmptyClass: 
     
    153151        f.close() 
    154152 
    155 def _HTTPErrorTemplate(errorString, message, traceback, version): 
    156     subTuple = (errorString, errorString, message, traceback, cherrypy.__version__) 
    157      
    158     return '''<?xml version="1.0" encoding="UTF-8"?> 
    159     <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
    160       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
    161     <html> 
    162     <head> 
    163         <title>%s</title> 
    164         <style type="text/css"> 
    165         #poweredBy { 
    166             margin-top: 20px; 
    167             border-top: 2px solid black; 
    168             font-style: italic; 
    169         } 
    170  
    171         #traceback { 
    172             color: red; 
    173         } 
    174         </style> 
    175     </head> 
    176         <body> 
    177             <h2>%s</h2> 
    178             <p>%s</p> 
    179             <pre id="traceback">%s</pre> 
    180         <div id="poweredBy"> 
    181         <span>Powered by <a href="http://www.cherrypy.org">Cherrypy %s</a></span> 
    182         </div> 
    183         </body> 
    184     </html> 
    185     ''' % subTuple 
    186  
    187 def getErrorStatusAndPage(status, traceback = None): 
    188     statusString, message = responseCodes[status] 
    189     statusString = '%d %s' % (status, statusString) 
    190      
    191     if traceback is None: 
    192         traceback = '' 
    193         # get the traceback from formatExc 
    194         developmentMode = (cherrypy.config.get('server.environment') == 'development') 
    195         if cherrypy.config.get('server.showTracebacks') or developmentMode: 
    196             traceback = formatExc() 
    197      
    198     page = _HTTPErrorTemplate(statusString, message, traceback, cherrypy.__version__) 
    199      
    200     return statusString, page 
     153 
     154_HTTPErrorTemplate = '''<?xml version="1.0" encoding="UTF-8"?> 
     155<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
     156  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
     157<html> 
     158<head> 
     159    <title>%(status)s</title> 
     160    <style type="text/css"> 
     161    #poweredBy { 
     162        margin-top: 20px; 
     163        border-top: 2px solid black; 
     164        font-style: italic; 
     165    } 
     166 
     167    #traceback { 
     168        color: red; 
     169    } 
     170    </style> 
     171</head> 
     172    <body> 
     173        <h2>%(status)s</h2> 
     174        <p>%(message)s</p> 
     175        <pre id="traceback">%(traceback)s</pre> 
     176    <div id="poweredBy"> 
     177    <span>Powered by <a href="http://www.cherrypy.org">Cherrypy %(version)s</a></span> 
     178    </div> 
     179    </body> 
     180</html> 
     181''' 
     182 
     183def getErrorPage(status, **kwargs): 
     184    """Return an HTML page, containing a pretty error response. 
     185     
     186    status should be an int or a str. 
     187    kwargs will be interpolated into the page template. 
     188    """ 
     189     
     190    code, reason, message = cptools.validStatus(status) 
     191    errorPageFile = cherrypy.config.get('errorPage.%s' % code, '') 
     192    if errorPageFile: 
     193        template = file(errorPageFile, 'rb') 
     194    else: 
     195        template = _HTTPErrorTemplate 
     196     
     197    kwargs.setdefault('status', "%s %s" % (code, reason)) 
     198    kwargs.setdefault('message', '') 
     199    kwargs.setdefault('traceback', '') 
     200    kwargs.setdefault('version', cherrypy.__version__) 
     201    for k, v in kwargs.iteritems(): 
     202        if v is None: 
     203            kwargs[k] = "" 
     204     
     205    return template % kwargs 
    201206 
    202207def formatExc(exc=None): 
    203     """formatExc(exc=None) -> exc (or sys.exc_info), formatted.""" 
     208    """formatExc(exc=None) -> exc (or sys.exc_info if None), formatted.""" 
    204209    if exc is None: 
    205210        exc = sys.exc_info() 
    206  
     211     
    207212    if exc == (None, None, None): 
    208213        return "" 
     
    211216def _cpOnError(): 
    212217    """ Default _cpOnError method """ 
    213      
    214     logTracebacks  = cherrypy.config.get('server.logTracebacks', True) 
    215     if logTracebacks: 
    216         cherrypy.log(formatExc()) 
    217      
    218     response = cherrypy.response 
    219      
    220     response.status, response.body = getErrorStatusAndPage(500) 
    221      
    222     if cherrypy.response.headerMap.has_key('Content-Encoding'): 
    223         del cherrypy.response.headerMap['Content-Encoding'] 
     218    cherrypy.HTTPError(500).set_response() 
    224219 
    225220_cpFilterList = [] 
  • trunk/cherrypy/lib/cptools.py

    r680 r682  
    2828 
    2929""" 
    30 Just a few convenient functions and classes 
     30Tools which both the CherryPy framework and application developers invoke. 
    3131""" 
     32 
     33from BaseHTTPServer import BaseHTTPRequestHandler 
     34responseCodes = BaseHTTPRequestHandler.responses 
    3235 
    3336import inspect 
     
    4144import sys 
    4245import time 
     46 
     47 
    4348import cherrypy 
    4449 
     
    327332        chunk = input.read(chunkSize) 
    328333    input.close() 
     334 
     335def validStatus(status): 
     336    """Return legal HTTP status Code, Reason-phrase and Message. 
     337     
     338    The status arg must be an int, or a str that begins with an int. 
     339     
     340    If status is an int, or a str and  no reason-phrase is supplied, 
     341    a default reason-phrase will be provided. 
     342    """ 
     343     
     344    if not status: 
     345        status = 200 
     346     
     347    status = str(status) 
     348    parts = status.split(" ", 1) 
     349    if len(parts) == 1: 
     350        # No reason supplied. 
     351        code, = parts 
     352        reason = None 
     353    else: 
     354        code, reason = parts 
     355        reason = reason.strip() 
     356     
     357    try: 
     358        code = int(code) 
     359    except ValueError: 
     360        raise cherrypy.HTTPError(500, "Illegal response status from server (non-numeric).") 
     361     
     362    if code < 100 or code > 599: 
     363        raise cherrypy.HTTPError(500, "Illegal response status from server (out of range).") 
     364     
     365    if code not in responseCodes: 
     366        # code is unknown but not illegal 
     367        defaultReason, message = "", "" 
     368    else: 
     369        defaultReason, message = responseCodes[code] 
     370     
     371    if reason is None: 
     372        reason = defaultReason 
     373     
     374    return code, reason, message 
  • trunk/cherrypy/test/helper.py

    r679 r682  
    150150        else: 
    151151            webtest.WebCase.getPage(self, url, headers, method, body) 
    152   
    153     def assertErrorPage(self, errorCode, pattern = ''): 
     152     
     153    def assertErrorPage(self, status, message=None, pattern=''): 
    154154        """ Compare the response body with a built in error page. 
    155155            The function will optionally look for the regexp pattern,  
    156156            within the exception embedded in the error page. 
    157157        """ 
    158  
    159         from cherrypy._cputil import getErrorStatusAndPage 
    160         page = getErrorStatusAndPage(errorCode, '')[1] 
    161  
    162         # escape the question marks 
    163         page = page.replace('?', r'\?') 
    164          
    165         # re to find the traceback in the page 
    166         traceRe = re.compile('(<pre id="traceback">)(</pre>)') 
    167  
    168         # stick the pattern in the page so we can match everythign 
    169         # at once 
    170         page = traceRe.sub( '\g<1>.*%s.*\g<2>' % pattern, page) 
    171          
    172         # check if there is no exception 
    173         if pattern and traceRe.search(self.body): 
    174             msg = 'No match for %s in body' % `pattern` 
    175             self._handlewebError(msg) 
     158         
     159        from cherrypy._cputil import getErrorPage 
     160        esc = re.escape 
     161        page = esc(getErrorPage(status, message=message)) 
     162         
     163        # First, test the response body without checking the traceback. 
     164        # Stick a match-all group (.*) in to grab the traceback. 
     165        page = page.replace(esc('<pre id="traceback"></pre>'), 
     166                            esc('<pre id="traceback">') + '(.*)' + esc('</pre>')) 
     167        m = re.match(page, self.body, re.DOTALL) 
     168        if not m: 
     169            self._handlewebError('Error page does not match') 
     170            return 
     171         
     172        # Now test the pattern against the traceback 
     173        if pattern is None: 
     174            # Special-case None to mean that there should be *no* traceback. 
     175            if m and m.group(1): 
     176                self._handlewebError('Error page contains traceback') 
    176177        else: 
    177             if not re.search(page, self.body, re.DOTALL): 
    178                 msg = 'Error page does not match' 
    179                 self._handlewebError(msg) 
     178            if (m is None) or (not re.search(pattern, m.group(1))): 
     179                msg = 'Error page does not contain %s in traceback' 
     180                self._handlewebError(msg % repr(pattern)) 
     181 
    180182 
    181183CPTestLoader = webtest.ReloadingTestLoader() 
  • trunk/cherrypy/test/test_core.py

    r679 r682  
    295295    '/error/page_streamed': { 
    296296        'streamResponse': True, 
     297    }, 
     298    '/error/cause_err_in_finalize': { 
     299        'server.showTracebacks': False, 
    297300    }, 
    298301}) 
     
    364367         
    365368        self.getPage("/status/illegal") 
    366         self.assertBody('oops' + (" " * 509)) 
    367369        self.assertStatus('500 Internal error') 
     370        msg = "Illegal response status from server (out of range)." 
     371        self.assertErrorPage(500, msg) 
    368372         
    369373        self.getPage("/status/unknown") 
     
    372376         
    373377        self.getPage("/status/bad") 
    374         self.assertBody('hello' + (" " * 508)) 
    375378        self.assertStatus('500 Internal error') 
     379        msg = "Illegal response status from server (non-numeric)." 
     380        self.assertErrorPage(500, msg) 
    376381     
    377382    def testLogging(self): 
     
    499504        self.getPage("/error/missing") 
    500505        self.assertStatus("404 Not Found") 
    501         self.assertErrorPage(404
     506        self.assertErrorPage(404, "The path '/error/missing' was not found."
    502507         
    503508        ignore = helper.webtest.ignored_exceptions 
     
    506511            valerr = r'\n    raise ValueError\(\)\nValueError\n' 
    507512            self.getPage("/error/page_method") 
    508             self.assertErrorPage(500, valerr) 
     513            self.assertErrorPage(500, pattern=valerr) 
    509514             
    510515            self.getPage("/error/page_yield") 
    511             self.assertErrorPage(500, valerr) 
     516            self.assertErrorPage(500, pattern=valerr) 
    512517             
    513518            import cherrypy 
     
    523528                self.assertBody("helloUnrecoverable error in the server.") 
    524529             
     530            # No traceback should be present 
    525531            self.getPage("/error/cause_err_in_finalize") 
    526             # We're in 'production' mode, so body should be empty 
    527             self.assertBody(""
     532            msg = "Illegal response status from server (non-numeric)." 
     533            self.assertErrorPage(500, msg, None
    528534        finally: 
    529535            ignore.pop() 
  • trunk/cherrypy/test/test_gzip_filter.py

    r679 r682  
    8181            self.assertNoHeader('Content-Encoding') 
    8282            self.assertStatus('500 Internal error') 
    83             self.assertErrorPage(500, "IndexError\n") 
     83            self.assertErrorPage(500, pattern="IndexError\n") 
    8484             
    8585            # In this case, there's nothing we can do to deliver a 

Hosted by WebFaction

Log in as guest/cpguest to create tickets