Repository: jupyterhub/jupyterhub

Issue:
CI: jupyter-server build fails since late september
The `test_singleuser_auth` step fails with the following error ([example failure](https://travis-ci.org/github/jupyterhub/jupyterhub/jobs/729518444))

```
404 Client Error: Not Found for url: http://127.0.0.1:59471/@/space%20word/user/nandy/api/spec.yaml?redirects=2
```

Has something change with regards to `@` symbols or spaces in words like `space word`? Yes it has, in `jupyter-server` it seems, because there have been releases in this time span.

![image](https://user-images.githubusercontent.com/3837114/94790428-9275a400-03d6-11eb-807f-1a174e1b3721.png)

## References
- [jupyter-server changelog](https://github.com/jupyter/jupyter_server/blob/master/CHANGELOG.md)
- [The only PR that I saw in the changelog with clear potential to cause our CI error](https://github.com/jupyter/jupyter_server/pull/304)
- [A seemingly related PR by, @minrk](https://github.com/jupyterhub/jupyterhub/pull/3168)
- [Another seemingly related PR, by @danlester](https://github.com/jupyterhub/jupyterhub/pull/3167)


Discussion:
I've narrowed it down a bit

Passes:
```
pip install jupyter_server==1.0.0rc16
JUPYTERHUB_SINGLEUSER_APP=jupyterhub.tests.mockserverapp.MockServerApp pytest jupyterhub/tests/test_singleuser.py
```

Hangs:
```
pip install jupyter_server==1.0.0
JUPYTERHUB_SINGLEUSER_APP=jupyterhub.tests.mockserverapp.MockServerApp pytest jupyterhub/tests/test_singleuser.py
```

Fails:
```
pip install jupyter_server==1.0.1
JUPYTERHUB_SINGLEUSER_APP=jupyterhub.tests.mockserverapp.MockServerApp pytest jupyterhub/tests/test_singleuser.py
```

`1.0.0rc16` - `1.0.0`: https://github.com/jupyter/jupyter_server/compare/1.0.0rc16...1.0.0

`1.0.0` - `1.0.1`: https://github.com/jupyter/jupyter_server/compare/1.0.0...1.0.1
Enabling verbose output:

`JUPYTERHUB_SINGLEUSER_APP=jupyterhub.tests.mockserverapp.MockServerApp pytest jupyterhub/tests/test_singleuser.py -k test_singleuser_auth -vs`

I suspect the URL encoding is a red herring, and the [addition of some missing templates](https://github.com/jupyter/jupyter_server/commit/49a16a7a362af1543a737011e703537a739976da) has changed the behaviour. Note the line `Using contents: services/contents` and onwards in `1.0.1` that's missing from `1.0.0rc16`:

`1.0.1`:
```
...
[I 2020-10-01 21:11:14.043 MockSingleUserServer auth:985] Logged-in user {'kind': 'user', 'name': 'nandy', 'admin': False, 'groups': [], 'server': '/@/space%20word/user/nandy/', 'pending': None, 'created': '2020-10-01T20:11:11.731209Z', 'last_activity': '2020-10-01T20:11:14.039860Z', 'servers': None}
[I 2020-10-01 21:11:14.044 MockSingleUserServer log:181] 302 GET /@/space%20word/user/nandy/oauth_callback?code=[secret]&state=[secret] -> /@/space%20word/user/nandy/api/spec.yaml?redirects=2 (@127.0.0.1) 31.25ms
[W 11:14.049 MockHub handlers:23] Serving api spec (experimental, incomplete)
[D 11:14.049 MockHub handlers:241] Using contents: services/contents
[D 11:14.056 MockHub handlers:750] Path favicon.ico served from /home/USER/miniconda3/envs/jupyter-dev/lib/python3.8/site-packages/jupyter_server/static/favicon.ico
[W 2020-10-01 21:11:14.057 MockSingleUserServer log:181] 404 GET /@/space%20word/user/nandy/api/spec.yaml?redirects=2 (nandy@127.0.0.1) 8.59ms
['--ip=127.0.0.1', '--port=55511', '--SingleUserNotebookApp.default_url=/api/spec.yaml'] {'PATH': '/home/USER/.local/bin:/home/USER/bin:/home/USER/.local/bin:/home/USER/bin:/home/USER/miniconda3/envs/jupyter-dev/bin:/home/USER/miniconda3/condabin:/home/USER/.local/bin:/home/USER/bin:/usr/share/Modules/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin', 'CONDA_DEFAULT_ENV': 'jupyter-dev', 'LANG': 'en_GB.UTF-8', 'JUPYTERHUB_SINGLEUSER_APP': 'jupyterhub.tests.mockserverapp.MockServerApp', 'JUPYTERHUB_API_TOKEN': '0d56cfe431af4478a2ad3ed26cfdd3d1', 'JPY_API_TOKEN': '0d56cfe431af4478a2ad3ed26cfdd3d1', 'JUPYTERHUB_CLIENT_ID': 'jupyterhub-user-nandy', 'JUPYTERHUB_HOST': '', 'JUPYTERHUB_OAUTH_CALLBACK_URL': '/@/space%20word/user/nandy/oauth_callback', 'JUPYTERHUB_USER': 'nandy', 'JUPYTERHUB_SERVER_NAME': '', 'JUPYTERHUB_API_URL': 'http://127.0.0.1:8081/@/space%20word/hub/api', 'JUPYTERHUB_ACTIVITY_URL': 'http://127.0.0.1:8081/@/space%20word/hub/api/users/nandy/activity', 'JUPYTERHUB_BASE_URL': '/@/space%20word/', 'JUPYTERHUB_SERVICE_PREFIX': '/@/space%20word/user/nandy/', 'USER': 'nandy', 'HOME': '/tmp/nandy', 'SHELL': '/bin/bash'}
FAILED[I 11:14.133 MockHub proxy:281] Removing user nandy from proxy (/@/space%20word/user/nandy/)
...
```

`1.0.0rc16`:
```
...
[I 2020-10-01 21:11:28.985 MockSingleUserServer auth:985] Logged-in user {'kind': 'user', 'name': 'nandy', 'admin': False, 'groups': [], 'server': '/@/space%20word/user/nandy/', 'pending': None, 'created': '2020-10-01T20:11:26.733923Z', 'last_activity': '2020-10-01T20:11:28.981594Z', 'servers': None}
[I 2020-10-01 21:11:28.985 MockSingleUserServer log:181] 302 GET /@/space%20word/user/nandy/oauth_callback?code=[secret]&state=[secret] -> /@/space%20word/user/nandy/api/spec.yaml?redirects=2 (@127.0.0.1) 27.55ms
[W 11:28.991 MockHub handlers:23] Serving api spec (experimental, incomplete)
[I 2020-10-01 21:11:28.997 MockSingleUserServer log:181] 302 GET /@/space%20word/user/nandy/logout -> /@/space%20word/hub/logout (@127.0.0.1) 0.68ms
[I 11:29.002 MockHub login:43] User logged out: nandy
[D 11:29.004 MockHub base:483] Deleted 1 access tokens for nandy
...
```

Resolving patch:
diff --git a/jupyterhub/traitlets.py b/jupyterhub/traitlets.py
--- a/jupyterhub/traitlets.py
+++ b/jupyterhub/traitlets.py
@@ -9,6 +9,7 @@
 from traitlets import TraitError
 from traitlets import TraitType
 from traitlets import Type
+from traitlets import Undefined
 from traitlets import Unicode
 
 
@@ -27,11 +28,15 @@ class Command(List):
     but allows it to be specified as a single string.
     """
 
-    def __init__(self, default_value=None, **kwargs):
+    def __init__(self, default_value=Undefined, **kwargs):
         kwargs.setdefault('minlen', 1)
         if isinstance(default_value, str):
             default_value = [default_value]
-        super().__init__(Unicode(), default_value, **kwargs)
+        if default_value is not Undefined and (
+            not (default_value is None and not kwargs.get("allow_none", False))
+        ):
+            kwargs["default_value"] = default_value
+        super().__init__(Unicode(), **kwargs)
 
     def validate(self, obj, value):
         if isinstance(value, str):


Test patch:
diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py
--- a/jupyterhub/tests/mocking.py
+++ b/jupyterhub/tests/mocking.py
@@ -403,7 +403,7 @@ def _default_url(self):
         - authenticated, so we are testing auth
         - always available (i.e. in base ServerApp and NotebookApp
         """
-        return "/api/spec.yaml"
+        return "/api/status"
 
     _thread = None