Repository: readthedocs/readthedocs.org
Issue: Redirect full path to default version

I'd like to be able to create versionless links to the documentation, for use in error messages, code comments, etc. For example, a message like `see https://click.palletsprojects.com/windows for more information`. I don't want to use URLs with versions because I would have to remember to modify all instances of it before releasing a new version.

Currently, only the root path redirects to the default version, other paths raise a 404. Instead, the path should be preserved and appended to the default version path on redirect.

```
Works:
https://click.palletsprojects.com/ -> https://click.palletsprojects.com/en/7.x/

Doesn't work, 404:
https://click.palletsprojects.com/windows -> https://click.palletsprojects.com/en/7.x/windows
```

I do not want to use the "latest" or "stable" versions because I would like the URLs that people land on and share to contain the actual version.

I already do this with the transitional redirects I set up from `click.pocoo.org` to `click.palletsprojects.com`. A similar approach could probably be used to extend RTD's default redirect.

```nginx
location ~ ^/dev(.*)$ {
  return 301 https://click.palletsprojects.com/en/master$1;
}

location ~ ^/(\\d)(.*)$ {
  return 301 https://click.palletsprojects.com/en/$1.x$2;
}

location ~ ^/latest(.*)$ {
  return 301 https://click.palletsprojects.com/en/7.x$1;
}

location / {
  return 301 https://click.palletsprojects.com/en/7.x$request_uri;
}
```

Changed files:
readthedocs/core/views/__init__.py, readthedocs/rtd_tests/tests/test_redirects.py

diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py
index e97a7247b..342eb5304 100644
--- a/readthedocs/core/views/__init__.py
+++ b/readthedocs/core/views/__init__.py
@@ -116,8 +116,16 @@ def server_error_404(request, exception=None, template_name='404.html'):  # pyli
         Marking exception as optional to make /404/ testing page to work.
     """
     response = get_redirect_response(request, path=request.get_full_path())
+
     if response:
-        return response
+        if response.url == request.build_absolute_uri():
+            # check that we do have a response and avoid infinite redirect
+            log.warning(
+                'Infinite Redirect: FROM URL is the same than TO URL. url=%s',
+                response.url,
+            )
+        else:
+            return response
     r = render(request, template_name)
     r.status_code = 404
     return r
diff --git a/readthedocs/rtd_tests/tests/test_redirects.py b/readthedocs/rtd_tests/tests/test_redirects.py
index 7595d1475..335c8feba 100644
--- a/readthedocs/rtd_tests/tests/test_redirects.py
+++ b/readthedocs/rtd_tests/tests/test_redirects.py
@@ -127,6 +127,34 @@ class RedirectAppTests(TestCase):
         self.pip = Project.objects.get(slug='pip')
         self.pip.versions.create_latest()
 
+    @override_settings(USE_SUBDOMAIN=True)
+    def test_redirect_prefix_infinite(self):
+        """
+        Avoid infinite redirects.
+
+        If the URL hit is the same that the URL returned for redirection, we
+        return a 404.
+
+        These examples comes from this issue:
+          * https://github.com/rtfd/readthedocs.org/issues/4673
+        """
+        Redirect.objects.create(
+            project=self.pip, redirect_type='prefix',
+            from_url='/',
+        )
+        r = self.client.get('/redirect', HTTP_HOST='pip.readthedocs.org')
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(
+            r['Location'], 'http://pip.readthedocs.org/en/latest/redirect.html')
+
+        r = self.client.get('/redirect/', HTTP_HOST='pip.readthedocs.org')
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(
+            r['Location'], 'http://pip.readthedocs.org/en/latest/redirect/')
+
+        r = self.client.get('/en/latest/redirect/', HTTP_HOST='pip.readthedocs.org')
+        self.assertEqual(r.status_code, 404)
+
     @override_settings(USE_SUBDOMAIN=True)
     def test_redirect_root(self):
         Redirect.objects.create(