diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index 753af60a..8bc72184 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -30,6 +30,9 @@ class APIHandler(BaseHandler): def content_security_policy(self): return '; '.join([super().content_security_policy, "default-src 'none'"]) + def get_content_type(self): + return 'application/json' + def check_referer(self): """Check Origin for cross-site API requests. @@ -265,3 +268,13 @@ class APIHandler(BaseHandler): def options(self, *args, **kwargs): self.finish() + + +class API404(APIHandler): + """404 for API requests + + Ensures JSON 404 errors for malformed URLs + """ + async def prepare(self): + await super().prepare() + raise web.HTTPError(404) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 1f1ba959..bec5bf3a 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -973,6 +973,8 @@ class JupyterHub(Application): h.extend(self.extra_handlers) h.append((r'/logo', LogoHandler, {'path': self.logo_file})) + h.append((r'/api/(.*)', apihandlers.base.API404)) + self.handlers = self.add_url_prefix(self.hub_prefix, h) # some extra handlers, outside hub_prefix self.handlers.extend([ diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 4699a40e..019e7bf2 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -100,6 +100,8 @@ def api_request(app, *api_path, **kwargs): assert "frame-ancestors 'self'" in resp.headers['Content-Security-Policy'] assert ujoin(app.hub.base_url, "security/csp-report") in resp.headers['Content-Security-Policy'] assert 'http' not in resp.headers['Content-Security-Policy'] + if not kwargs.get('stream', False) and resp.content: + assert resp.headers.get('content-type') == 'application/json' return resp @@ -746,6 +748,8 @@ def test_progress(request, app, no_patience, slow_spawn): r = yield api_request(app, 'users', name, 'server/progress', stream=True) r.raise_for_status() request.addfinalizer(r.close) + assert r.headers['content-type'] == 'text/event-stream' + ex = async_requests.executor line_iter = iter(r.iter_lines(decode_unicode=True)) evt = yield ex.submit(next_event, line_iter) @@ -807,6 +811,7 @@ def test_progress_ready(request, app): r = yield api_request(app, 'users', name, 'server/progress', stream=True) r.raise_for_status() request.addfinalizer(r.close) + assert r.headers['content-type'] == 'text/event-stream' ex = async_requests.executor line_iter = iter(r.iter_lines(decode_unicode=True)) evt = yield ex.submit(next_event, line_iter) @@ -826,6 +831,7 @@ def test_progress_bad(request, app, no_patience, bad_spawn): r = yield api_request(app, 'users', name, 'server/progress', stream=True) r.raise_for_status() request.addfinalizer(r.close) + assert r.headers['content-type'] == 'text/event-stream' ex = async_requests.executor line_iter = iter(r.iter_lines(decode_unicode=True)) evt = yield ex.submit(next_event, line_iter) @@ -847,6 +853,7 @@ def test_progress_bad_slow(request, app, no_patience, slow_bad_spawn): r = yield api_request(app, 'users', name, 'server/progress', stream=True) r.raise_for_status() request.addfinalizer(r.close) + assert r.headers['content-type'] == 'text/event-stream' ex = async_requests.executor line_iter = iter(r.iter_lines(decode_unicode=True)) evt = yield ex.submit(next_event, line_iter)