Merge pull request #5146 from agoose77/fix-spawn-429

set HTTP status when spawn via GET params fails
This commit is contained in:
Min RK
2025-10-03 09:28:04 -07:00
committed by GitHub
4 changed files with 66 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ from ..roles import assign_default_roles
from ..scopes import needs_scope from ..scopes import needs_scope
from ..user import User from ..user import User
from ..utils import ( from ..utils import (
format_exception,
isoformat, isoformat,
iterate_until, iterate_until,
maybe_future, maybe_future,
@@ -865,9 +866,8 @@ class SpawnProgressAPIHandler(APIHandler):
failed_event['message'] = "Spawn cancelled" failed_event['message'] = "Spawn cancelled"
elif f and f.done() and f.exception(): elif f and f.done() and f.exception():
exc = 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}" failed_event['message'] = f"Spawn failed: {message}"
html_message = getattr(exc, "jupyterhub_html_message", "")
if html_message: if html_message:
failed_event['html_message'] = html_message failed_event['html_message'] = html_message
else: else:
@@ -906,9 +906,8 @@ class SpawnProgressAPIHandler(APIHandler):
failed_event['message'] = "Spawn cancelled" failed_event['message'] = "Spawn cancelled"
elif f and f.done() and f.exception(): elif f and f.done() and f.exception():
exc = 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}" failed_event['message'] = f"Spawn failed: {message}"
html_message = getattr(exc, "jupyterhub_html_message", "")
if html_message: if html_message:
failed_event['html_message'] = html_message failed_event['html_message'] = html_message
else: else:

View File

@@ -15,7 +15,13 @@ from tornado.httputil import url_concat
from .. import __version__, orm from .. import __version__, orm
from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus
from ..scopes import describe_raw_scopes, needs_scope 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 from .base import BaseHandler
@@ -92,7 +98,9 @@ class SpawnHandler(BaseHandler):
default_url = None 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() auth_state = await for_user.get_auth_state()
return await self.render_template( return await self.render_template(
'spawn.html', 'spawn.html',
@@ -100,6 +108,7 @@ class SpawnHandler(BaseHandler):
auth_state=auth_state, auth_state=auth_state,
spawner_options_form=spawner_options_form, spawner_options_form=spawner_options_form,
error_message=message, error_message=message,
html_error_message=html_message,
url=url_concat( url=url_concat(
self.request.uri, {"_xsrf": self.xsrf_token.decode('ascii')} self.request.uri, {"_xsrf": self.xsrf_token.decode('ascii')}
), ),
@@ -177,7 +186,6 @@ class SpawnHandler(BaseHandler):
await spawner.run_auth_state_hook(auth_state) await spawner.run_auth_state_hook(auth_state)
# Try to start server directly when query arguments are passed. # Try to start server directly when query arguments are passed.
error_message = ''
query_options = {} query_options = {}
for key, byte_list in self.request.query_arguments.items(): for key, byte_list in self.request.query_arguments.items():
query_options[key] = [bs.decode('utf8') for bs in byte_list] 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 # 'next' is reserved argument for redirect after spawn
query_options.pop('next', None) query_options.pop('next', None)
spawn_exc = None
if len(query_options) > 0: if len(query_options) > 0:
try: try:
self.log.debug( self.log.debug(
@@ -200,16 +210,31 @@ class SpawnHandler(BaseHandler):
"Failed to spawn single-user server with query arguments", "Failed to spawn single-user server with query arguments",
exc_info=True, exc_info=True,
) )
error_message = str(e) spawn_exc = e
# fallback to behavior without failing query arguments # fallback to behavior without failing query arguments
spawner_options_form = await spawner.get_options_form() spawner_options_form = await spawner.get_options_form()
if spawner_options_form: if spawner_options_form:
self.log.debug("Serving options form for %s", spawner._log_name) 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( form = await self._render_form(
for_user=user, for_user=user,
spawner_options_form=spawner_options_form, spawner_options_form=spawner_options_form,
message=error_message, message=error_message,
html_message=error_html_message,
) )
self.finish(form) self.finish(form)
else: else:
@@ -265,9 +290,23 @@ class SpawnHandler(BaseHandler):
self.log.error( self.log.error(
"Failed to spawn single-user server with form", exc_info=True "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() spawner_options_form = await user.spawner.get_options_form()
form = await self._render_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) self.finish(form)
return return
@@ -379,6 +418,8 @@ class SpawnPendingHandler(BaseHandler):
if isinstance(exc, web.HTTPError): if isinstance(exc, web.HTTPError):
status_code = exc.status_code status_code = exc.status_code
self.set_status(status_code) self.set_status(status_code)
message, html_message = format_exception(exc, only_jupyterhub=True)
html = await self.render_template( html = await self.render_template(
"not_running.html", "not_running.html",
user=user, user=user,
@@ -386,8 +427,8 @@ class SpawnPendingHandler(BaseHandler):
server_name=server_name, server_name=server_name,
spawn_url=spawn_url, spawn_url=spawn_url,
failed=True, failed=True,
failed_html_message=getattr(exc, 'jupyterhub_html_message', ''), failed_html_message=html_message,
failed_message=getattr(exc, 'jupyterhub_message', ''), failed_message=message,
exception=exc, exception=exc,
) )
self.finish(html) self.finish(html)

View File

@@ -984,3 +984,13 @@ def fmt_ip_url(ip):
if ":" in ip: if ":" in ip:
return f"[{ip}]" return f"[{ip}]"
return 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
)

View File

@@ -14,7 +14,11 @@
{% if for_user and user.name != for_user.name -%} {% if for_user and user.name != for_user.name -%}
<p>Spawning server for {{ for_user.name }}</p> <p>Spawning server for {{ for_user.name }}</p>
{% endif -%} {% endif -%}
{% if error_message -%}<p class="spawn-error-msg alert alert-danger">Error: {{ error_message }}</p>{% endif %} {% if error_message %}
<p class="spawn-error-msg alert alert-danger">Error: {{ error_message }}</p>
{% elif error_html_message %}
<p class="spawn-error-msg alert alert-danger">{{ error_html_message | safe }}</p>
{% endif %}
<form enctype="multipart/form-data" <form enctype="multipart/form-data"
id="spawn_form" id="spawn_form"
action="{{ url | safe }}" action="{{ url | safe }}"