Merge pull request #4735 from minrk/better-errors

add some HTML error pages
This commit is contained in:
Thomas Kluyver 2014-01-09 13:03:31 -08:00
commit c0f346c4e3
10 changed files with 151 additions and 17 deletions

View File

@ -25,7 +25,13 @@ import re
import stat
import sys
import traceback
try:
# py3
from http.client import responses
except ImportError:
from httplib import responses
from jinja2 import TemplateNotFound
from tornado import web
try:
@ -46,14 +52,7 @@ UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
#-----------------------------------------------------------------------------
non_alphanum = re.compile(r'[^A-Za-z0-9]')
class RequestHandler(web.RequestHandler):
"""RequestHandler with default variable setting."""
def render(*args, **kwargs):
kwargs.setdefault('message', '')
return web.RequestHandler.render(*args, **kwargs)
class AuthenticatedHandler(RequestHandler):
class AuthenticatedHandler(web.RequestHandler):
"""A RequestHandler with an authenticated user."""
def clear_login_cookie(self):
@ -212,6 +211,45 @@ class IPythonHandler(AuthenticatedHandler):
raise web.HTTPError(400, u'Invalid JSON in body of request')
return model
def get_error_html(self, status_code, **kwargs):
"""render custom error pages"""
exception = kwargs.get('exception')
message = ''
status_message = responses.get(status_code, 'Unknown HTTP Error')
if exception:
# get the custom message, if defined
try:
message = exception.log_message % exception.args
except Exception:
pass
# construct the custom reason, if defined
reason = getattr(exception, 'reason', '')
if reason:
status_message = reason
# build template namespace
ns = dict(
status_code=status_code,
status_message=status_message,
message=message,
exception=exception,
)
# render the template
try:
html = self.render_template('%s.html' % status_code, **ns)
except TemplateNotFound:
self.log.debug("No template for %d", status_code)
html = self.render_template('error.html', **ns)
return html
class Template404(IPythonHandler):
"""Render our 404 template"""
def prepare(self):
raise web.HTTPError(404)
class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
"""static files should only be accessible when logged in"""

View File

@ -6,7 +6,7 @@ from tornado import web
from ..base.handlers import IPythonHandler, notebook_path_regex
from IPython.nbformat.current import to_notebook_json
from IPython.nbconvert.exporters.export import exporter_map
from IPython.utils import tz
from IPython.utils.py3compat import cast_bytes
@ -47,13 +47,33 @@ def respond_zip(handler, name, output, resources):
handler.finish(buffer.getvalue())
return True
def get_exporter(format, **kwargs):
"""get an exporter, raising appropriate errors"""
# if this fails, will raise 500
try:
from IPython.nbconvert.exporters.export import exporter_map
except ImportError as e:
raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
try:
Exporter = exporter_map[format]
except KeyError:
# should this be 400?
raise web.HTTPError(404, u"No exporter for format: %s" % format)
try:
return Exporter(**kwargs)
except Exception as e:
raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
class NbconvertFileHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET',)
@web.authenticated
def get(self, format, path='', name=None):
exporter = exporter_map[format](config=self.config)
exporter = get_exporter(format, config=self.config)
path = path.strip('/')
os_path = self.notebook_manager.get_os_path(name, path)
@ -62,8 +82,11 @@ class NbconvertFileHandler(IPythonHandler):
info = os.stat(os_path)
self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime))
output, resources = exporter.from_filename(os_path)
try:
output, resources = exporter.from_filename(os_path)
except Exception as e:
raise web.HTTPError(500, "nbconvert failed: %s" % e)
if respond_zip(self, name, output, resources):
return
@ -86,12 +109,15 @@ class NbconvertPostHandler(IPythonHandler):
@web.authenticated
def post(self, format):
exporter = exporter_map[format](config=self.config)
exporter = get_exporter(format, config=self.config)
model = self.get_json_body()
nbnode = to_notebook_json(model['content'])
output, resources = exporter.from_notebook_node(nbnode)
try:
output, resources = exporter.from_notebook_node(nbnode)
except Exception as e:
raise web.HTTPError(500, "nbconvert failed: %s" % e)
if respond_zip(self, nbnode.metadata.name, output, resources):
return

View File

@ -61,6 +61,7 @@ from tornado import web
# Our own libraries
from IPython.html import DEFAULT_STATIC_FILES_PATH
from .base.handlers import Template404
from .services.kernels.kernelmanager import MappingKernelManager
from .services.notebooks.nbmanager import NotebookManager
@ -208,6 +209,8 @@ class NotebookWebApplication(web.Application):
pattern = url_path_join(settings['base_project_url'], handler[0])
new_handler = tuple([pattern] + list(handler[1:]))
new_handlers.append(new_handler)
# add 404 on the end, which will catch everything that falls through
new_handlers.append((r'(.*)', Template404))
return new_handlers

View File

@ -3,7 +3,6 @@ import json
from tornado import web
from ...base.handlers import IPythonHandler, json_errors
from IPython.nbconvert.exporters.export import exporter_map
class NbconvertRootHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET',)
@ -11,6 +10,10 @@ class NbconvertRootHandler(IPythonHandler):
@web.authenticated
@json_errors
def get(self):
try:
from IPython.nbconvert.exporters.export import exporter_map
except ImportError as e:
raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
res = {}
for format, exporter in exporter_map.items():
res[format] = info = {}

View File

@ -0,0 +1,20 @@
div.error {
margin: 2em;
text-align: center;
}
div.error > h1 {
font-size: 500%;
line-height: normal;
}
div.error > p {
font-size: 200%;
line-height: normal;
}
div.traceback-wrapper {
text-align: left;
max-width: 800px;
margin: auto;
}

View File

@ -1,4 +1,4 @@
@import "variables.less";
@import "mixins.less";
@import "flexbox.less";
@import "error.less";

View File

@ -18,6 +18,10 @@
.start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;}
.end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;}
.center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;}
div.error{margin:2em;text-align:center;}
div.error>h1{font-size:500%;line-height:normal;}
div.error>p{font-size:200%;line-height:normal;}
div.traceback-wrapper{text-align:left;max-width:800px;margin:auto;}
.center-nav{display:inline-block;margin-bottom:-4px;}
.alternate_upload{background-color:none;display:inline;}
.alternate_upload.form{padding:0;margin:0;}

View File

@ -1385,6 +1385,10 @@ ul.icons-ul{list-style-type:none;text-indent:-0.7142857142857143em;margin-left:2
.start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;}
.end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;}
.center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;}
div.error{margin:2em;text-align:center;}
div.error>h1{font-size:500%;line-height:normal;}
div.error>p{font-size:200%;line-height:normal;}
div.traceback-wrapper{text-align:left;max-width:800px;margin:auto;}
body{background-color:white;position:absolute;left:0px;right:0px;top:0px;bottom:0px;overflow:visible;}
div#header{display:none;}
#ipython_notebook{padding-left:16px;}

View File

@ -0,0 +1,5 @@
{% extends "error.html" %}
{% block error_detail %}
<p>You are requesting a page that does not exist!</p>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block stylesheet %}
{{super()}}
<style type="text/css">
/* disable initial hide */
div#header, div#site {
display: block;
}
</style>
{% endblock %}
{% block site %}
<div class="error">
{% block h1_error %}
<h1>{{status_code}} : {{status_message}}</h1>
{% endblock h1_error %}
{% block error_detail %}
{% if message %}
<p>The error was:</p>
<div class="traceback-wrapper">
<pre class="traceback">{{message}}</pre>
</div>
{% endif %}
{% endblock %}
</header>
{% endblock %}