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;
}
```


Discussion:
I think this is what you need https://github.com/rtfd/readthedocs.org/issues/2422#issuecomment-417764518
Although, you'll need to change the redirect on each release
An exact redirect from `/$rest`, or a prefix redirect from `/`, to `/en/1.0.x` works, but as a side effect it causes infinite redirects for 404s. If the page is https://itsdangerous.palletsprojects.com/en/1.0.x/signer, `/signer` redirects correctly, but `/en/`, `/en/bad-version`, and `/en/1.0.x/bad-page` all just keep getting appended and redirected. The first two shouldn't really come up, but the last one is an issue because the user sees a strange error instead of a 404.

The redirects catch too much, they should only trigger for 404s that don't already start with the `/$lang/$version/` prefix being redirected to.
I'd rather not have to update these redirects as new releases occur either, as part of the draw of moving to RTD was that we could drop our custom bots and processes for building docs.
@davidism your issue called my attention because I thought that we supported this.

I found a _hacky solution_, I think.

This URL, 
* https://gh-rtd-project-a.readthedocs.io/redirect, will bring you to
* https://gh-rtd-project-a.readthedocs.io/en/redirect/redirect.html (being `/en` the default language and `redirect` the default version) --which I suppose it's what you want.

How I did it? I created a `Prefix Redirect` with `From URL` as just `/`.

This brought other issues: 

* it produces an infinite redirection with trailing `/`, https://gh-rtd-project-a.readthedocs.io/redirect/
* I found that leaving the `From URL` as empty is possible to submit and it doesn't make any sense, producing all 404 URLs to infinite redirection

----

From your examples,

```
location ~ ^/dev(.*)$ {
  return 301 https://click.palletsprojects.com/en/master$1;
}
```

This one can be achieved by an `Exact Redirect` with the `$rest` attribute, I think. 

* From URL: `/dev/$rest`
* To URL: `/en/master/`

----

Similar Redirect can be done for this one,

```
location ~ ^/latest(.*)$ {
  return 301 https://click.palletsprojects.com/en/7.x$1;
}
```

* From URL: `/latest/$rest`
* To URL: `/en/7.x/`


----

The _hacky way_ that I found should solve this one (although, it should be implemented in a better way),

```
location / {
  return 301 https://click.palletsprojects.com/en/7.x$request_uri;
}
```

----

Finally, maybe this can be achieved by

* From URL: `/$rest`
* To URL: `/en/`

```
location ~ ^/(\d)(.*)$ {
  return 301 https://click.palletsprojects.com/en/$1.x$2;
}
```

So, accessing `/1.x/section/file.html` will redirect to `/en/1.x/section/file.html`

----

I'm listing all of this here but I know it doesn't sound like a "good answer". I want to be sure that, even with a _hacky way_ we can make this work or not. This will probably need some changes in our implementation of redirects --which is kind of complicated.


Finally, if you add a _namespace_ in your the URLs you want to link, like `/redirect-to-default-version/` and add a `Prefix Redirect` this will work:

https://gh-rtd-project-a.readthedocs.io/redirect-to-default-version/redirect.html

With a shorter namespace,

https://gh-rtd-project-a.readthedocs.io/r/redirect.html

So, your URLs will be in the form https://click.palletsprojects.com/r/windows

Does any of this make sense to you? :grin: 

Thanks, I started using prefix and exact redirects in an earlier comment. The problem with all these is that any 404 now produces an infinite redirect with a weird repeated URL. It's not end-user friendly.
> The problem with all these is that any 404 now produces an infinite redirect with a weird repeated URL. It's not end-user friendly.

If that bug is fixed and just returns a 404, that would be enough for your use case?
Yeah, I think that should cover it.

Resolving patch:
diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py
--- 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


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_redirects.py b/readthedocs/rtd_tests/tests/test_redirects.py
--- a/readthedocs/rtd_tests/tests/test_redirects.py
+++ b/readthedocs/rtd_tests/tests/test_redirects.py
@@ -127,6 +127,34 @@ def setUp(self):
         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(