Re: Nice looking error pages

classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|

Re: Nice looking error pages

gasolin@gmail.com


Felix Schwarz 寫道:

> Hi all,
>
> so far I good only positive feedback regarding the error page :-)
> But most people probably just looked at the screenshots, I don't think
> that anyone did a real code review.
>
> What can I do now in order to get an implementation of my prototype
> into the main CherryPy code? I even prefer to get a straight rejection
> of my proposal than a long period of uncertainty.
>
> As written earlier, the according trac ticket is
> http://www.cherrypy.org/ticket/576.
>
> --
> Felix


Hi:

For Ticket #576, is it possible to add an exception dispatcher or hook
to customize this ?
(pass out the origin exception to dispatcher, let the dispatcher parse
and render the output page)

Then the complex html could be seperated from the core, and developers
could customize their own error pages.

I haven't dive into code much, but it seems not too hard though.

186 186        response.status = self.status
187        tb = None
188        if cherrypy.request.show_tracebacks:
189            tb = format_exc()
190        content = get_error_page(self.status, traceback=tb,


--
Fred


--~--~---------~--~----~------------~-------~--~----~
 You received this message because you are subscribed to the Google Groups "cherrypy-devel" group.
To post to this group, send email to [hidden email]
To unsubscribe from this group, send email to [hidden email]
For more options, visit this group at http://groups-beta.google.com/group/cherrypy-devel
-~----------~----~----~----~------~----~------~--~---

Reply | Threaded
Open this post in threaded view
|

Re:Nice looking error pages

Felix Schwarz


gasolin wrote:
> For Ticket #576, is it possible to add an exception dispatcher or hook
> to customize this ?
> (pass out the origin exception to dispatcher, let the dispatcher parse
> and render the output page)

Incidentally, I just talked to Robert Brewer about his proposals in ticket 576. He came up
with a kind of an API which will separate the complex html nicely from the rest of cherrypy.

I'll look into this in a few days.

fs

--~--~---------~--~----~------------~-------~--~----~
 You received this message because you are subscribed to the Google Groups "cherrypy-devel" group.
To post to this group, send email to [hidden email]
To unsubscribe from this group, send email to [hidden email]
For more options, visit this group at http://groups-beta.google.com/group/cherrypy-devel
-~----------~----~----~----~------~----~------~--~---

Reply | Threaded
Open this post in threaded view
|

Re: Nice looking error pages

Felix Schwarz
In reply to this post by gasolin@gmail.com

Second version. The error page formatting is now separated from
_cperror. Unit tests need more work (cleanup, helper functions).
Number of context lines can be specified by request.context_lines.

Unfortunately, overriding _cperror.get_error_page() breaks several
unit tests. Therefore I like to introduce a new configuration option
(with a default value!) to choose an error formatter. Suggestions for
a name?

--
Felix

--~--~---------~--~----~------------~-------~--~----~
 You received this message because you are subscribed to the Google Groups "cherrypy-devel" group.
To post to this group, send email to [hidden email]
To unsubscribe from this group, send email to [hidden email]
For more options, visit this group at http://groups-beta.google.com/group/cherrypy-devel
-~----------~----~----~----~------~----~------~--~---

"""Error formatting class to create error pages"""

from cgi import escape as _escape
import sys
import traceback

import cherrypy
from cherrypy.lib import http

import _cperror


_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">
    body {
        font-family: Verdana, Arial, Sans-Serif;
        margin: 0;
        padding: 0;
    }

    /* have a little bit of space around the screen but prevent inheritance
     * to non-direct children of #content
     */
    #content * { padding: 10px 10px; }
    #content * * { padding:0; }

    #overview_top {
    background: #f3ebba;
    border-bottom: 1px solid #c1bdbd;
    }

    h1 {
    font-size: 2em;
    font-weight: normal;
    margin: 0px;
    }

    h2 {
    font-size: 1em;
    font-weight: normal;
    margin: 0px;
    }
   
    #powered_by {
        height: 2em;
        padding-top: 0.2em;

        font-style: italic;
        background: #f3ebba;
        border-top: 1px solid #c1bdbd;
        border-bottom: 1px solid #c1bdbd;
    }

    #footertext { margin-left: 10px; } /* same as in "#content *" */

    pre {
    margin: 0px;
    }
   
    #currentLine {
    background: #eeeeee;
    font-weight: bold;
    }

    #traceback {
        font-size: 1.1em;
    margin-top: 0px;
        color: red;
    }
    </style>

</head>

<body>
        <div id="content">

                <div id="overview_top">
                        <h1>
                                %(status)s
                        </h1>
                        <h2>
                                %(message)s
                        </h2>
                </div>
               
                %(details)s
        </div>
        <br/><br/>
   
        <div id="powered_by">
            <span id="footertext">Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
        </div>
</body>
</html>
"""

_500_ERRORDETAILS_TEMPLATE = """<div>In file %(filename)s, line %(lineno)s:<br/>
<pre>%(code_before_line)s</pre>
<pre id=\"currentLine\">%(current_line)s</pre>
<pre>%(code_after_line)s</pre>
</div>
<div>
   complete traceback: <br/>
   <pre id=\"traceback\">%(traceback)s</pre>
</div>
"""


def _escape_all_values(dic):
    for k, v in dic.iteritems():
        if v is None:
            dic[k] = ""
        else:
            dic[k] = _escape(dic[k])


def _fill_kwargs(title, message, kwargs):
    # We can't use setdefault here, because some
    # callers send None for kwarg values.
    if kwargs.get('status') is None:
        kwargs['status'] = "%s" % title
    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__


def _get_valid_status(status):
    try:
        code, reason, message = http.valid_status(status)
        return code, reason, message
    except ValueError, x:
        raise cherrypy.HTTPError(500, x.args[0])


def _get_cause(stack_trace_entries):
    if stack_trace_entries != None and len(stack_trace_entries) > 0:
        return stack_trace_entries[len(stack_trace_entries)-1]
    return (None, 1, None, None)


def _get_lines_from_file(filename, line_number, number_of_context_lines):
        source = open(filename).readlines()
        min_index = max(0, line_number - number_of_context_lines - 1) # -1 because list indexes start at zero unlike file numbers
        max_index = min(len(source) - 1, line_number + number_of_context_lines)
        stripped_source = map(lambda x: x.strip(), source[min_index:max_index])
        return (min_index+1, source[min_index:max_index]) # +1 because line numbers start as 1 unlike list indexes


def _format_source_lines(line_number, lines):
    output = ""
    for line in lines:
        output += str(line_number) + "\t" + line
        line_number += 1
    return output


def _get_number_of_context_lines():
    lines = 5
    try:
        lines = int(cherrypy.request.context_lines)
    except (AttributeError, ValueError, TypeError):
        pass
    return lines


def _get_source_context(stack_trace_entries):
    (filename, lineno, methodname, source) = _get_cause(stack_trace_entries)
    if filename != None:
        number_of_context_lines = _get_number_of_context_lines()
        (context_line_number, source_lines) = _get_lines_from_file(filename, lineno, number_of_context_lines)
        current_line_index = min(number_of_context_lines, lineno - 1)
        precontext = _format_source_lines(context_line_number, source_lines[:current_line_index])
        exception_line = str(lineno) + "\t" + source_lines[current_line_index]
        postcontext = _format_source_lines(lineno+1, source_lines[current_line_index+1:])
        return (filename, lineno, precontext, exception_line, postcontext)
    return ("unknown", lineno, "", "", "")


def _build_detailed_exception(exception_info):
    (exception_type, exception_value, trace) = exception_info
    values = {}
    stack_trace_entries = traceback.extract_tb(trace)
    (filename, line_number, values["code_before_line"], values["current_line"], values["code_after_line"]) = \
        _get_source_context(stack_trace_entries)
    values["filename"] = filename
    values["lineno"] = str(line_number)
    values['traceback'] = _cperror.format_exc(exception_info)
    _escape_all_values(values)
    return _500_ERRORDETAILS_TEMPLATE % values


def _build_500_error_page():
    exception_info = sys.exc_info()
    (title, explanation, escaped_error_information) = ("", "", "")
    if exception_info != None:
        (exception_type, exception_value, trace) = exception_info
        title = exception_type.__name__
        explanation = str(exception_value)
        escaped_error_information = _build_detailed_exception(exception_info)
    return (title, explanation, escaped_error_information)


def _extract_status_information(status):
    code, reason, message = _get_valid_status(status)
    title = "%s %s" % (code, reason)
    explanation = message
    return (code, title, explanation)


def get_error_page(status, **kwargs):
    code, title, explanation = _extract_status_information(status)
    escaped_error_information = ""
    if cherrypy.request.show_tracebacks:
        if code == 500:
            (title, explanation, escaped_error_information) = _build_500_error_page()
   
    _fill_kwargs(title, explanation, kwargs)
    _escape_all_values(kwargs)
    kwargs["details"] = escaped_error_information
    return _HTTPErrorTemplate % kwargs


def init():
    _cperror.get_error_page = get_error_page

import inspect

from cherrypy.test import test
test.prefer_parent_path()

import cherrypy

class Root:
    def index(self):
        assert False
    index.exposed = True

    def typeError(self):
        "" + 1
        return "Should not reach this"
    typeError.exposed = True

def setup_server():
    root = Root()
    cherrypy.config.update({
        'environment': 'test_suite',
    })
    cherrypy.tree.mount(root, config={})


from cherrypy.test import helper


class ErrorFormatterTests(helper.CPWebCase):

    def setUp(self):
        super(ErrorFormatterTests, self)
        filename = __file__
        if filename.endswith("pyc"):
           filename = filename[:len(filename)-1]
        self.filename_test = filename
        cherrypy.config["request.show_tracebacks"] = True

    def testAssertionError(self):
        self.getPage('/')
        self.assertStatus("500 Internal Server Error")

        title = "AssertionError"
        self.assertInBody("<title>%s</title>" % title)
        self.assertMatchesBody("<h1>\s*%s\s*</h1>" % title)
        self.assertMatchesBody("<h2>\s*</h2>")
        line_number = inspect.getsourcelines(Root.index)[1] + 1
        self.assertMatchesBody("<div>In file %s, line %d:<br/>" % (self.filename_test, line_number))
        self.assertInBody("<pre>%d" % (line_number - 5))


    def testTypeError(self):
        context_size = 10
        cherrypy.config["request.context_lines"] = context_size
        self.getPage('/typeError')
        self.assertStatus("500 Internal Server Error")

        title = "TypeError"
        self.assertInBody("<title>%s</title>" % title)
        self.assertMatchesBody("<h1>\s*%s\s*</h1>" % title)
        self.assertMatchesBody("<h2>\s*cannot concatenate 'str' and 'int' objects\s*</h2>")
        line_number = inspect.getsourcelines(Root.typeError)[1] + 1
        self.assertInBody("<div>In file %s, line %d:<br/>" % (self.filename_test, line_number))
        self.assertInBody("<pre>%d" % (line_number - context_size))
        self.assertMatchesBody('<pre id="currentLine">%d\s*"" \+ 1' % line_number)
        self.assertInBody("<pre>%d" % (line_number + 1))
        self.assertMatchesBody("TypeError: cannot concatenate 'str' and 'int' objects\s+</pre>")


    def testHideTracebacks(self):
        cherrypy.config["request.show_tracebacks"] = False
        self.getPage('/typeError')
        statusline = "500 Internal Server Error"
        self.assertStatus(statusline)
        self.assertInBody("<title>%s</title>" % statusline)
        self.assertMatchesBody("<h1>\s*%s\s*</h1>" % statusline)
        self.assertMatchesBody("<h2>\s*The server encountered an unexpected condition which prevented it from fulfilling the request.\s*</h2>")
        self.assertNotInBody("<div>In file %s, line" % self.filename_test)


if __name__ == "__main__":
    setup_server()
    helper.testmain()