Repository: dask/distributed

Issue:
[UI][FEATURE] Invalid worker or task keys should return 404 instead of 500 HTML status code
E.g. http://host:8787/info/task/foobared.html returns `500: Internal Server Error`.

Should instead return a friendlier 404 page.


Discussion:
Sounds good to me.  You seemed interested in contributing to the dashboard earlier.  Does that still apply here?  If so, here are some possibly useful places to start looking:

Our routes are here:

https://github.com/dask/distributed/blob/051a79e05c501dcea57a19715f59d0cd203f6be6/distributed/dashboard/scheduler_html.py#L231-L251

And they get added to this Tornado web server (which is hiding within a Bokeh server)

https://github.com/dask/distributed/blob/051a79e05c501dcea57a19715f59d0cd203f6be6/distributed/dashboard/core.py#L51
Meaning that `(r"info/task/(.*).html", Task), ` picks up that URL, but the corresponding handler errors. You would get a 404 if you missed the routes altogether, but the handlers should indeed do that too, if you pass a URL that looks valid, but the target isn't found; in this case "No such task" or something.

@rs2 , we would be happy to see this as a PR.

Resolving patch:
diff --git a/distributed/dashboard/scheduler_html.py b/distributed/dashboard/scheduler_html.py
--- a/distributed/dashboard/scheduler_html.py
+++ b/distributed/dashboard/scheduler_html.py
@@ -27,6 +27,9 @@ def get(self):
 class Worker(RequestHandler):
     def get(self, worker):
         worker = escape.url_unescape(worker)
+        if worker not in self.server.workers:
+            self.send_error(404)
+            return
         with log_errors():
             self.render(
                 "worker.html",
@@ -40,6 +43,9 @@ def get(self, worker):
 class Task(RequestHandler):
     def get(self, task):
         task = escape.url_unescape(task)
+        if task not in self.server.tasks:
+            self.send_error(404)
+            return
         with log_errors():
             self.render(
                 "task.html",


Test patch:
diff --git a/distributed/dashboard/tests/test_scheduler_bokeh_html.py b/distributed/dashboard/tests/test_scheduler_bokeh_html.py
--- a/distributed/dashboard/tests/test_scheduler_bokeh_html.py
+++ b/distributed/dashboard/tests/test_scheduler_bokeh_html.py
@@ -6,7 +6,7 @@
 pytest.importorskip("bokeh")
 
 from tornado.escape import url_escape
-from tornado.httpclient import AsyncHTTPClient
+from tornado.httpclient import AsyncHTTPClient, HTTPClientError
 
 from dask.sizeof import sizeof
 from distributed.utils import is_valid_xml
@@ -49,6 +49,25 @@ def test_connect(c, s, a, b):
             assert not re.search("href=./", body)  # no absolute links
 
 
+@gen_cluster(
+    client=True,
+    nthreads=[],
+    scheduler_kwargs={"services": {("dashboard", 0): BokehScheduler}},
+)
+def test_worker_404(c, s):
+    http_client = AsyncHTTPClient()
+    with pytest.raises(HTTPClientError) as err:
+        yield http_client.fetch(
+            "http://localhost:%d/info/worker/unknown" % s.services["dashboard"].port
+        )
+    assert err.value.code == 404
+    with pytest.raises(HTTPClientError) as err:
+        yield http_client.fetch(
+            "http://localhost:%d/info/task/unknown" % s.services["dashboard"].port
+        )
+    assert err.value.code == 404
+
+
 @gen_cluster(
     client=True,
     scheduler_kwargs={