diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index a08ae08f..d8fbcfb2 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -24,6 +24,7 @@ from ..roles import assign_default_roles from ..scopes import needs_scope from ..user import User from ..utils import ( + format_exception, isoformat, iterate_until, maybe_future, @@ -865,9 +866,8 @@ class SpawnProgressAPIHandler(APIHandler): failed_event['message'] = "Spawn cancelled" elif f and f.done() and f.exception(): exc = f.exception() - message = getattr(exc, "jupyterhub_message", str(exc)) + message, html_message = format_exception(exc) failed_event['message'] = f"Spawn failed: {message}" - html_message = getattr(exc, "jupyterhub_html_message", "") if html_message: failed_event['html_message'] = html_message else: @@ -906,9 +906,8 @@ class SpawnProgressAPIHandler(APIHandler): failed_event['message'] = "Spawn cancelled" elif f and f.done() and f.exception(): exc = f.exception() - message = getattr(exc, "jupyterhub_message", str(exc)) + message, html_message = format_exception(exc) failed_event['message'] = f"Spawn failed: {message}" - html_message = getattr(exc, "jupyterhub_html_message", "") if html_message: failed_event['html_message'] = html_message else: diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 31182dbc..198185f7 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -15,7 +15,13 @@ from tornado.httputil import url_concat from .. import __version__, orm from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus from ..scopes import describe_raw_scopes, needs_scope -from ..utils import maybe_future, url_escape_path, url_path_join, utcnow +from ..utils import ( + format_exception, + maybe_future, + url_escape_path, + url_path_join, + utcnow, +) from .base import BaseHandler @@ -92,7 +98,9 @@ class SpawnHandler(BaseHandler): default_url = None - async def _render_form(self, for_user, spawner_options_form, message=''): + async def _render_form( + self, for_user, spawner_options_form, message='', html_message='' + ): auth_state = await for_user.get_auth_state() return await self.render_template( 'spawn.html', @@ -100,6 +108,7 @@ class SpawnHandler(BaseHandler): auth_state=auth_state, spawner_options_form=spawner_options_form, error_message=message, + html_error_message=html_message, url=url_concat( self.request.uri, {"_xsrf": self.xsrf_token.decode('ascii')} ), @@ -177,7 +186,6 @@ class SpawnHandler(BaseHandler): await spawner.run_auth_state_hook(auth_state) # Try to start server directly when query arguments are passed. - error_message = '' query_options = {} for key, byte_list in self.request.query_arguments.items(): query_options[key] = [bs.decode('utf8') for bs in byte_list] @@ -185,6 +193,8 @@ class SpawnHandler(BaseHandler): # 'next' is reserved argument for redirect after spawn query_options.pop('next', None) + spawn_exc = None + if len(query_options) > 0: try: self.log.debug( @@ -200,16 +210,31 @@ class SpawnHandler(BaseHandler): "Failed to spawn single-user server with query arguments", exc_info=True, ) - error_message = str(e) + spawn_exc = e # fallback to behavior without failing query arguments spawner_options_form = await spawner.get_options_form() if spawner_options_form: self.log.debug("Serving options form for %s", spawner._log_name) + + # Explicitly catch HTTPError and report them to the client + # This may need scoping to particular error codes. + if isinstance(spawn_exc, web.HTTPError): + self.set_status(spawn_exc.status_code) + + for name, value in spawn_exc.headers.items(): + self.set_header(name, value) + + if spawn_exc: + error_message, error_html_message = format_exception(spawn_exc) + else: + error_message = error_html_message = None + form = await self._render_form( for_user=user, spawner_options_form=spawner_options_form, message=error_message, + html_message=error_html_message, ) self.finish(form) else: @@ -265,9 +290,23 @@ class SpawnHandler(BaseHandler): self.log.error( "Failed to spawn single-user server with form", exc_info=True ) + + # Explicitly catch HTTPError and report them to the client + # This may need scoping to particular error codes. + if isinstance(e, web.HTTPError): + self.set_status(e.status_code) + + for name, value in e.headers.items(): + self.set_header(name, value) + + error_message, error_html_message = format_exception(e) + spawner_options_form = await user.spawner.get_options_form() form = await self._render_form( - for_user=user, spawner_options_form=spawner_options_form, message=str(e) + for_user=user, + spawner_options_form=spawner_options_form, + message=error_message, + html_message=error_html_message, ) self.finish(form) return @@ -379,6 +418,8 @@ class SpawnPendingHandler(BaseHandler): if isinstance(exc, web.HTTPError): status_code = exc.status_code self.set_status(status_code) + + message, html_message = format_exception(exc, only_jupyterhub=True) html = await self.render_template( "not_running.html", user=user, @@ -386,8 +427,8 @@ class SpawnPendingHandler(BaseHandler): server_name=server_name, spawn_url=spawn_url, failed=True, - failed_html_message=getattr(exc, 'jupyterhub_html_message', ''), - failed_message=getattr(exc, 'jupyterhub_message', ''), + failed_html_message=html_message, + failed_message=message, exception=exc, ) self.finish(html) diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index aaefe7df..d2b888fc 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -984,3 +984,13 @@ def fmt_ip_url(ip): if ":" in ip: return f"[{ip}]" return ip + + +def format_exception(exc, *, only_jupyterhub=False): + """ + Format an exception into a text string and HTML pair. + """ + default_message = None if only_jupyterhub else str(exc) + return getattr(exc, "jupyterhub_message", default_message), getattr( + exc, "jupyterhub_html_message", None + ) diff --git a/share/jupyterhub/templates/spawn.html b/share/jupyterhub/templates/spawn.html index b55018c9..f8d4b4ad 100644 --- a/share/jupyterhub/templates/spawn.html +++ b/share/jupyterhub/templates/spawn.html @@ -14,7 +14,11 @@ {% if for_user and user.name != for_user.name -%}
Spawning server for {{ for_user.name }}
{% endif -%} - {% if error_message -%}Error: {{ error_message }}
{% endif %} + {% if error_message %} +Error: {{ error_message }}
+ {% elif error_html_message %} +{{ error_html_message | safe }}
+ {% endif %}