mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
Merge pull request #5020 from minrk/custom_error
make sure custom error messages are shown on regular error pages
This commit is contained in:
@@ -1487,6 +1487,7 @@ class BaseHandler(RequestHandler):
|
|||||||
"""render custom error pages"""
|
"""render custom error pages"""
|
||||||
exc_info = kwargs.get('exc_info')
|
exc_info = kwargs.get('exc_info')
|
||||||
message = ''
|
message = ''
|
||||||
|
message_html = ''
|
||||||
exception = None
|
exception = None
|
||||||
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||||
if exc_info:
|
if exc_info:
|
||||||
@@ -1496,12 +1497,17 @@ class BaseHandler(RequestHandler):
|
|||||||
message = exception.log_message % exception.args
|
message = exception.log_message % exception.args
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# allow custom html messages
|
||||||
|
message_html = getattr(exception, "jupyterhub_html_message", "")
|
||||||
|
|
||||||
# construct the custom reason, if defined
|
# construct the custom reason, if defined
|
||||||
reason = getattr(exception, 'reason', '')
|
reason = getattr(exception, 'reason', '')
|
||||||
if reason:
|
if reason:
|
||||||
message = reasons.get(reason, reason)
|
message = reasons.get(reason, reason)
|
||||||
|
|
||||||
|
# get special jupyterhub_message, if defined
|
||||||
|
message = getattr(exception, "jupyterhub_message", message)
|
||||||
|
|
||||||
if exception and isinstance(exception, SQLAlchemyError):
|
if exception and isinstance(exception, SQLAlchemyError):
|
||||||
self.log.warning("Rolling back session due to database error %s", exception)
|
self.log.warning("Rolling back session due to database error %s", exception)
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
@@ -1511,6 +1517,7 @@ class BaseHandler(RequestHandler):
|
|||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
status_message=status_message,
|
status_message=status_message,
|
||||||
message=message,
|
message=message,
|
||||||
|
message_html=message_html,
|
||||||
extra_error_html=getattr(self, 'extra_error_html', ''),
|
extra_error_html=getattr(self, 'extra_error_html', ''),
|
||||||
exception=exception,
|
exception=exception,
|
||||||
)
|
)
|
||||||
|
@@ -375,7 +375,10 @@ class SpawnPendingHandler(BaseHandler):
|
|||||||
spawn_url = url_path_join(
|
spawn_url = url_path_join(
|
||||||
self.hub.base_url, "spawn", user.escaped_name, escaped_server_name
|
self.hub.base_url, "spawn", user.escaped_name, escaped_server_name
|
||||||
)
|
)
|
||||||
self.set_status(500)
|
status_code = 500
|
||||||
|
if isinstance(exc, web.HTTPError):
|
||||||
|
status_code = exc.status_code
|
||||||
|
self.set_status(status_code)
|
||||||
html = await self.render_template(
|
html = await self.render_template(
|
||||||
"not_running.html",
|
"not_running.html",
|
||||||
user=user,
|
user=user,
|
||||||
|
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
from contextlib import nullcontext
|
||||||
|
from functools import partial
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import parse_qs, urlencode, urlparse
|
from urllib.parse import parse_qs, urlencode, urlparse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
from tornado import web
|
||||||
from tornado.httputil import url_concat
|
from tornado.httputil import url_concat
|
||||||
|
|
||||||
from .. import orm, roles, scopes
|
from .. import orm, roles, scopes
|
||||||
@@ -1335,3 +1338,86 @@ async def test_services_nav_links(
|
|||||||
assert service.href in nav_urls
|
assert service.href in nav_urls
|
||||||
else:
|
else:
|
||||||
assert service.href not in nav_urls
|
assert service.href not in nav_urls
|
||||||
|
|
||||||
|
|
||||||
|
class TeapotError(web.HTTPError):
|
||||||
|
text = "I'm a <🫖>"
|
||||||
|
html = "<b>🕸️🫖</b>"
|
||||||
|
|
||||||
|
def __init__(self, log_msg, kind="text"):
|
||||||
|
super().__init__(418, log_msg)
|
||||||
|
self.jupyterhub_message = self.text
|
||||||
|
if kind == "html":
|
||||||
|
self.jupyterhub_html_message = self.html
|
||||||
|
|
||||||
|
|
||||||
|
def hook_fail_fast(spawner, kind):
|
||||||
|
if kind == "unhandled":
|
||||||
|
raise RuntimeError("unhandle me!!!")
|
||||||
|
raise TeapotError("log_msg", kind=kind)
|
||||||
|
|
||||||
|
|
||||||
|
async def hook_fail_slow(spawner, kind):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
hook_fail_fast(spawner, kind)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("speed", ["fast", "slow"])
|
||||||
|
@pytest.mark.parametrize("kind", ["text", "html", "unhandled"])
|
||||||
|
async def test_spawn_fails_custom_message(app, user, kind, speed):
|
||||||
|
if speed == 'slow':
|
||||||
|
speed_context = mock.patch.dict(
|
||||||
|
app.tornado_settings, {'slow_spawn_timeout': 0.1}
|
||||||
|
)
|
||||||
|
hook = hook_fail_slow
|
||||||
|
else:
|
||||||
|
speed_context = nullcontext()
|
||||||
|
hook = hook_fail_fast
|
||||||
|
# test the response when spawn fails before redirecting to progress
|
||||||
|
with mock.patch.dict(
|
||||||
|
app.config.Spawner, {"pre_spawn_hook": partial(hook, kind=kind)}
|
||||||
|
), speed_context:
|
||||||
|
cookies = await app.login_user(user.name)
|
||||||
|
assert user.spawner.pre_spawn_hook
|
||||||
|
r = await get_page("spawn", app, cookies=cookies)
|
||||||
|
if speed == "slow":
|
||||||
|
# go through spawn_pending, render not_running.html
|
||||||
|
assert r.ok
|
||||||
|
assert "spawn-pending" in r.url
|
||||||
|
# wait for ready signal before checking next redirect
|
||||||
|
while user.spawner.active:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
app.log.info(
|
||||||
|
f"pending {user.spawner.active=}, {user.spawner._spawn_future=}"
|
||||||
|
)
|
||||||
|
# this should fetch the not-running page
|
||||||
|
app.log.info("getting again")
|
||||||
|
r = await get_page(
|
||||||
|
f"spawn-pending/{user.escaped_name}", app, cookies=cookies
|
||||||
|
)
|
||||||
|
target_class = "container"
|
||||||
|
unhandled_text = "Spawn failed"
|
||||||
|
else:
|
||||||
|
unhandled_text = "Unhandled error"
|
||||||
|
target_class = "error"
|
||||||
|
page = BeautifulSoup(r.content)
|
||||||
|
if kind == "unhandled":
|
||||||
|
assert r.status_code == 500
|
||||||
|
else:
|
||||||
|
assert r.status_code == 418
|
||||||
|
error = page.find(class_=target_class)
|
||||||
|
# check escaping properly
|
||||||
|
error_html = str(error)
|
||||||
|
if kind == "text":
|
||||||
|
assert "<🫖>" in error.text
|
||||||
|
assert "🕸️" not in error.text
|
||||||
|
assert "<🫖>" in error_html
|
||||||
|
elif kind == "html":
|
||||||
|
assert "<🫖>" not in error.text
|
||||||
|
assert "🕸️" in error.text
|
||||||
|
assert "<b>🕸️🫖</b>" in error_html
|
||||||
|
elif kind == "unhandled":
|
||||||
|
assert unhandled_text in error.text
|
||||||
|
assert "unhandle me" not in error.text
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unexpected {kind=}")
|
||||||
|
@@ -7,8 +7,11 @@
|
|||||||
<h1>{{ status_code }} : {{ status_message }}</h1>
|
<h1>{{ status_code }} : {{ status_message }}</h1>
|
||||||
{% endblock h1_error %}
|
{% endblock h1_error %}
|
||||||
{% block error_detail %}
|
{% block error_detail %}
|
||||||
{% if message %}<p>{{ message }}</p>{% endif %}
|
{% if message_html %}
|
||||||
{% if message_html %}<p>{{ message_html | safe }}</p>{% endif %}
|
<p>{{ message_html | safe }}</p>
|
||||||
|
{% elif message %}
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
{% endif %}
|
||||||
{% if extra_error_html %}<p>{{ extra_error_html | safe }}</p>{% endif %}
|
{% if extra_error_html %}<p>{{ extra_error_html | safe }}</p>{% endif %}
|
||||||
{% endblock error_detail %}
|
{% endblock error_detail %}
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user