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 ..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:

View File

@@ -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)

View File

@@ -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
)

View File

@@ -14,7 +14,11 @@
{% if for_user and user.name != for_user.name -%}
<p>Spawning server for {{ for_user.name }}</p>
{% 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"
id="spawn_form"
action="{{ url | safe }}"