diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index e2f938f20..888584c3c 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -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""" diff --git a/IPython/html/nbconvert/handlers.py b/IPython/html/nbconvert/handlers.py index b7197a717..2fb6f4586 100644 --- a/IPython/html/nbconvert/handlers.py +++ b/IPython/html/nbconvert/handlers.py @@ -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 diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 0dc0dc258..85b3fc26f 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -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 diff --git a/IPython/html/services/nbconvert/handlers.py b/IPython/html/services/nbconvert/handlers.py index e2ced712b..1c74de5d6 100644 --- a/IPython/html/services/nbconvert/handlers.py +++ b/IPython/html/services/nbconvert/handlers.py @@ -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 = {} diff --git a/IPython/html/static/base/less/error.less b/IPython/html/static/base/less/error.less new file mode 100644 index 000000000..0a1eadb0d --- /dev/null +++ b/IPython/html/static/base/less/error.less @@ -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; +} diff --git a/IPython/html/static/base/less/style.less b/IPython/html/static/base/less/style.less index 18af1959d..40c5b965a 100644 --- a/IPython/html/static/base/less/style.less +++ b/IPython/html/static/base/less/style.less @@ -1,4 +1,4 @@ @import "variables.less"; @import "mixins.less"; @import "flexbox.less"; - +@import "error.less"; diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index a26bd1f65..00d99e2dc 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -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;} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index 19f14f54d..46e64d584 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -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;} diff --git a/IPython/html/templates/404.html b/IPython/html/templates/404.html new file mode 100644 index 000000000..733505185 --- /dev/null +++ b/IPython/html/templates/404.html @@ -0,0 +1,5 @@ +{% extends "error.html" %} +{% block error_detail %} +

You are requesting a page that does not exist!

+{% endblock %} + diff --git a/IPython/html/templates/error.html b/IPython/html/templates/error.html new file mode 100644 index 000000000..bedf06c2e --- /dev/null +++ b/IPython/html/templates/error.html @@ -0,0 +1,31 @@ +{% extends "page.html" %} + +{% block login_widget %} +{% endblock %} + +{% block stylesheet %} +{{super()}} + +{% endblock %} +{% block site %} + +
+ {% block h1_error %} +

{{status_code}} : {{status_message}}

+ {% endblock h1_error %} + {% block error_detail %} + {% if message %} +

The error was:

+
+
{{message}}
+
+ {% endif %} + {% endblock %} + + +{% endblock %}