diff --git a/jupyterhub/app.py b/jupyterhub/app.py index ec52edfd..78f17366 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1518,6 +1518,25 @@ class JupyterHub(Application): """, ).tag(config=True) + use_legacy_stopped_server_status_code = Bool( + False, + help=""" + Return 503 rather than 424 when request comes in for a non-running server. + + Prior to JupyterHub 2.0, we returned a 503 when any request came in for + a user server that was currently not running. By default, JupyterHub 2.0 + will return a 424 - this makes operational metric dashboards more useful. + + JupyterLab < 3.2 expected the 503 to know if the user server is no longer + running, and prompted the user to start their server. Set this config to + true to retain the old behavior, so JupyterLab < 3.2 can continue to show + the appropriate UI when the user server is stopped. + + This option will be removed in a future release. + """, + config=True, + ) + def init_handlers(self): h = [] # load handlers from the authenticator diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index a2285937..258af3c4 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1357,7 +1357,7 @@ class UserUrlHandler(BaseHandler): **Changed Behavior as of 1.0** This handler no longer triggers a spawn. Instead, it checks if: - 1. server is not active, serve page prompting for spawn (status: 503) + 1. server is not active, serve page prompting for spawn (status: 424) 2. server is ready (This shouldn't happen! Proxy isn't updated yet. Wait a bit and redirect.) 3. server is active, redirect to /hub/spawn-pending to monitor launch progress (will redirect back when finished) @@ -1376,7 +1376,14 @@ class UserUrlHandler(BaseHandler): self.log.warning( "Failing suspected API request to not-running server: %s", self.request.path ) - self.set_status(503) + + # If we got here, the server is not running. To differentiate + # that the *server* itself is not running, rather than just the particular + # resource *in* the server is not found, we return a 424 instead of a 404. + # We allow retaining the old behavior to support older JupyterLab versions + self.set_status( + 424 if not self.app.use_legacy_stopped_server_status_code else 503 + ) self.set_header("Content-Type", "application/json") spawn_url = urlparse(self.request.full_url())._replace(query="") @@ -1541,15 +1548,17 @@ class UserUrlHandler(BaseHandler): self.redirect(pending_url, status=303) return - # if we got here, the server is not running - # serve a page prompting for spawn and 503 error - # visiting /user/:name no longer triggers implicit spawn - # without explicit user action + # If we got here, the server is not running. To differentiate + # that the *server* itself is not running, rather than just the particular + # page *in* the server is not found, we return a 424 instead of a 404. + # We allow retaining the old behavior to support older JupyterLab versions spawn_url = url_concat( url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name), {"next": self.request.uri}, ) - self.set_status(503) + self.set_status( + 424 if not self.app.use_legacy_stopped_server_status_code else 503 + ) auth_state = await user.get_auth_state() html = await self.render_template( diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 6c0290b2..27fbc533 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -56,8 +56,8 @@ async def test_root_redirect(app): r = await get_page(url, app, cookies=cookies) path = urlparse(r.url).path assert path == ujoin(app.base_url, 'hub/user/%s/test.ipynb' % name) - # serve "server not running" page, which has status 503 - assert r.status_code == 503 + # serve "server not running" page, which has status 424 + assert r.status_code == 424 async def test_root_default_url_noauth(app): @@ -172,7 +172,7 @@ async def test_spawn_redirect(app): r = await get_page('user/' + name, app, hub=False, cookies=cookies) path = urlparse(r.url).path assert path == ujoin(app.base_url, 'hub/user/%s/' % name) - assert r.status_code == 503 + assert r.status_code == 424 async def test_spawn_handler_access(app): @@ -507,13 +507,13 @@ async def test_user_redirect_deprecated(app, username): print(urlparse(r.url)) path = urlparse(r.url).path assert path == ujoin(app.base_url, 'hub/user/%s/' % name) - assert r.status_code == 503 + assert r.status_code == 424 r = await get_page('/user/baduser/test.ipynb', app, cookies=cookies, hub=False) print(urlparse(r.url)) path = urlparse(r.url).path assert path == ujoin(app.base_url, 'hub/user/%s/test.ipynb' % name) - assert r.status_code == 503 + assert r.status_code == 424 r = await get_page('/user/baduser/test.ipynb', app, hub=False) r.raise_for_status() @@ -1061,13 +1061,20 @@ async def test_token_page(app): async def test_server_not_running_api_request(app): cookies = await app.login_user("bees") r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies) - assert r.status_code == 503 + assert r.status_code == 424 assert r.headers["content-type"] == "application/json" message = r.json()['message'] assert ujoin(app.base_url, "hub/spawn/bees") in message assert " /user/bees" in message +async def test_server_not_running_api_request_legacy_status(app): + app.use_legacy_stopped_server_status_code = True + cookies = await app.login_user("bees") + r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies) + assert r.status_code == 503 + + async def test_metrics_no_auth(app): r = await get_page("metrics", app) assert r.status_code == 403