    async def _redirect_to_user_server(self, user, spawner):
        """Redirect from /hub/user/:name/... to /user/:name/...

        Can cause redirect loops if the proxy is malfunctioning.

        We do exponential backoff here - since otherwise we can get stuck in a redirect loop!
        This is important in many distributed proxy implementations - those are often eventually
        consistent and can take up to a couple of seconds to actually apply throughout the cluster.
        """
        try:
            redirects = int(self.get_argument('redirects', 0))
        except ValueError:
            self.log.warning(
                "Invalid redirects argument %r", self.get_argument('redirects')
            )
            redirects = 0

        # check redirect limit to prevent browser-enforced limits.
        # In case of version mismatch, raise on only two redirects.
        if redirects >= self.settings.get('user_redirect_limit', 4) or (
            redirects >= 2 and spawner._jupyterhub_version != __version__
        ):
            # We stop if we've been redirected too many times.
            msg = "Redirect loop detected."
            if spawner._jupyterhub_version != __version__:
                msg += (
                    " Notebook has jupyterhub version {singleuser}, but the Hub expects {hub}."
                    " Try installing jupyterhub=={hub} in the user environment"
                    " if you continue to have problems."
                ).format(
                    singleuser=spawner._jupyterhub_version or 'unknown (likely < 0.8)',
                    hub=__version__,
                )
            raise web.HTTPError(500, msg)

        without_prefix = self.request.uri[len(self.hub.base_url) :]
        target = url_path_join(self.base_url, without_prefix)
        if self.subdomain_host:
            target = user.host + target

        # record redirect count in query parameter
        if redirects:
            self.log.warning("Redirect loop detected on %s", self.request.uri)
            # add capped exponential backoff where cap is 10s
            await asyncio.sleep(min(1 * (2**redirects), 10))
            # rewrite target url with new `redirects` query value
            url_parts = urlparse(target)
            query_parts = parse_qs(url_parts.query)
            query_parts['redirects'] = redirects + 1
            url_parts = url_parts._replace(query=urlencode(query_parts, doseq=True))
            target = urlunparse(url_parts)
        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)
        self.statsd.incr('redirects.user_after_login')
Redirect from /hub/user/:name/... to /user/:name/...

Can cause redirect loops if the proxy is malfunctioning.

We do exponential backoff here - since otherwise we can get stuck in a redirect loop!
This is important in many distributed proxy implementations - those are often eventually
consistent and can take up to a couple of seconds to actually apply throughout the cluster.
jupyterhub/handlers/base.py