Repository: jupyterhub/jupyterhub

Issue:
Infinite redirect without backoff after accessing /spawn-pending/:user page
<!-- Thank you for contributing. These HTML comments will not render in the issue, but you can delete them once you've read them if you prefer! -->

### Bug description
<!-- Use this section to clearly and concisely describe the bug. -->
The user redirect counter is not being set if redirected from the `/spawn-pending/:user` page, which causes a redirect loop without exponential backoff, which causes Chrome to show the "too many redirects" error page

The bug seems to happen here: https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/handlers/base.py#L1590
When directed from the the `/spawn-pending/:user` page, the Referer header is set to this URL. It never changes.
Is there a reason why the `elif` clause exists? I think a simple fix would be to just replace it with an `else` clause.

I did a quick search it seems like the behavior of the Referer header for 302s is not defined:
https://stackoverflow.com/questions/2158283/will-a-302-redirect-maintain-the-referer-string
This behavior could differ between browsers.

#### Expected behaviour
<!-- Tell us what you thought would happen. -->
The browser should redirect with exponential backoff to prevent "too many redirects"

#### Actual behaviour
<!-- Tell us what actually happens. -->
The browser (Chrome) gets caught in a redirect loop with backoff, which causes it to show the "too many redirects" error page

### How to reproduce
<!-- Use this section to describe the steps that a user would take to experience this bug. -->
This error is tricky to reproduce because it depends on timing. The reason I am able to see this problem is because I use KubeSpawner to create notebook instances. KubeSpawner creates Kubernetes resources that are set up asynchronously by controllers, and the ingress resource for the notebook is set up by NGINX ingress controller. This means that the spawner could "finish" creating the notebook but the notebook's ingress may not be ready yet, which will cause /hub/spawn-pending/:user to redirect to /hub/user/:user instead of. /user/:user.

1. Log in to JupyterHub and spawn a notebook
2. After the progress bar fills up, you will be redirected to /user/:user which will redirect to /hub/user/:user
3. These redirects will happen rapidly until either your browser gives up, or the notebook's route finishes setting up
4. Either way, if you check Chrome's network tab, you will notice that you went through a redirect loop

### Your personal set up
<!--
Tell us a little about the system you're using.
Please include information about how you installed,
e.g. are you using a distribution such as zero-to-jupyterhub or the-littlest-jupyterhub.
 -->
Latest zero-to-jupyterhub release

 - OS:
 <!-- [e.g. ubuntu 20.04, macOS 11.0] -->
ubuntu 20.04
 - Version(s):
 <!-- e.g. jupyterhub --version, python --version --->
Jupyterhub 1.3.0 Python 3.8.5

- <details><summary>Full environment</summary>
<!-- For reproduction, it's useful to have the full environment. For example, the output of `pip freeze` or `conda list` --->

```
# paste output of `pip freeze` or `conda list` here
alembic==1.4.3
async-generator==1.10
attrs==20.3.0
bcrypt==3.2.0
cachetools==4.1.1
certifi==2020.12.5
certipy==0.1.3
cffi==1.14.4
chardet==3.0.4
cryptography==3.2.1
entrypoints==0.3
escapism==1.0.1
google-auth==1.23.0
idna==2.10
ipython-genutils==0.2.0
Jinja2==2.11.2
jsonschema==3.2.0
jupyter-telemetry==0.1.0
jupyterhub==1.3.0
jupyterhub-firstuseauthenticator==0.14.1
jupyterhub-hmacauthenticator==1.0
jupyterhub-idle-culler==1.0
jupyterhub-kubespawner==0.15.0
jupyterhub-ldapauthenticator==1.3.2
jupyterhub-ltiauthenticator==1.0.0
jupyterhub-nativeauthenticator==0.0.7
jupyterhub-tmpauthenticator==0.6
kubernetes==12.0.1
ldap3==2.8.1
Mako==1.1.3
MarkupSafe==1.1.1
mwoauth==0.3.7
nullauthenticator==1.0.0
oauthenticator==0.12.3
oauthlib==3.1.0
onetimepass==1.0.1
pamela==1.0.0
prometheus-client==0.9.0
psycopg2-binary==2.8.6
py-spy==0.3.3
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.20
pycurl==7.43.0.6
PyJWT==1.7.1
PyMySQL==0.10.1
pyOpenSSL==20.0.0
pyrsistent==0.17.3
python-dateutil==2.8.1
python-editor==1.0.4
python-json-logger==2.0.1
python-slugify==4.0.1
PyYAML==5.3.1
requests==2.25.0
requests-oauthlib==1.3.0
rsa==4.6
ruamel.yaml==0.16.12
ruamel.yaml.clib==0.2.2
six==1.15.0
SQLAlchemy==1.3.20
statsd==3.3.0
text-unidecode==1.3
tornado==6.1
traitlets==5.0.5
urllib3==1.26.2
websocket-client==0.57.0
```
</details>

- <details><summary>Configuration</summary>
<!--
For JupyterHub, especially include information such as what Spawner and Authenticator are being used.
Be careful not to share any sensitive information.
You can paste jupyterhub_config.py below.
To exclude lots of comments and empty lines from auto-generated jupyterhub_config.py, you can do:
    grep -v '\(^#\|^[[:space:]]*$\)' jupyterhub_config.py
-->
kubespawner 0.15.0
</details>

- <details><summary>Logs</summary>
<!--
Errors are often logged by jupytehub. How you get logs depends on your deployment.
With kubernetes it might be:

    kubectl get pod # hub pod name starts with hub...
    kubectl logs hub-...
    # or for a single-user server
    kubectl logs jupyter-username

Or the-littlest-jupyterhub:

    journalctl -u jupyterhub
    # or for a single-user server
    journalctl -u jupyter-username
-->
N/A
</details>



Discussion:
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:

I suspect this to be a configuration error, and without logs i cant tell.

What i guess is that your user is redirected from hub to /user/:user by the hub and then redirected again from configurable http proxy (proxy pod) to /hub/user/:user because it had not been properly configured with a route for the user yet.

I guess you may have two replicas of the proxy pod, or that the proxy pod was reset and lost its state and havent yet been updated by the hub pod. Your config of the jupyterhub helm chart and logs of hub/proxy pod are essential to conclude this is a bug if it would be a bug though.

Resolving patch:
diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py
--- a/jupyterhub/handlers/base.py
+++ b/jupyterhub/handlers/base.py
@@ -1575,7 +1575,6 @@ async def _redirect_to_user_server(self, user, spawner):
         if self.subdomain_host:
             target = user.host + target
 
-        referer = self.request.headers.get('Referer', '')
         # record redirect count in query parameter
         if redirects:
             self.log.warning("Redirect loop detected on %s", self.request.uri)
@@ -1587,8 +1586,12 @@ async def _redirect_to_user_server(self, user, spawner):
             query_parts['redirects'] = redirects + 1
             url_parts = url_parts._replace(query=urlencode(query_parts, doseq=True))
             target = urlunparse(url_parts)
-        elif '/user/{}'.format(user.name) in referer or not referer:
-            # add first counter only if it's a redirect from /user/:name -> /hub/user/:name
+        else:
+            # Start redirect counter.
+            # This should only occur for redirects from /user/:name -> /hub/user/:name
+            # when the corresponding server is already ready.
+            # We don't check this explicitly (direct visits to /hub/user are technically possible),
+            # but that's now the only normal way to get here.
             target = url_concat(target, {'redirects': 1})
 
         self.redirect(target)


Test patch: