Repository: readthedocs/readthedocs.org

Issue:
Split X-RTD-Version-Method header into two HTTP headers.
* Version-Method reflects how the version slug was obtained:
  * external_domain: slug from external versions' domain (split after --)
  * url: slug was taken directly from the URL
* Project-Method indicates how the project slug was obtained
  * rtdheader: X-RTD-SLUG header
  * subdomain: slug is the first part in the host before readthedocs.io
  * cname: slug is from a custom domain


Discussion:
This seems technically correct, but I don't know if it's useful. Having one header for all of these seems like the best UX, I don't want to be returning a million headers, so I'd just rename it `X-Project-Method` if you care about it. 

Resolving patch:
diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py
--- a/readthedocs/proxito/middleware.py
+++ b/readthedocs/proxito/middleware.py
@@ -112,6 +112,39 @@ class ProxitoMiddleware(MiddlewareMixin):
 
     """The actual middleware we'll be using in prod."""
 
+    def add_proxito_headers(self, request, response):
+        """Add debugging headers to proxito responses."""
+
+        project_slug = getattr(request, 'host_project_slug', '')
+        version_slug = getattr(request, 'path_version_slug', '')
+        path = getattr(response, 'proxito_path', '')
+
+        response['X-RTD-Domain'] = request.get_host()
+        response['X-RTD-Project'] = project_slug
+
+        if version_slug:
+            response['X-RTD-Version'] = version_slug
+
+        if path:
+            response['X-RTD-Path'] = path
+
+        # Include the project & project-version so we can do larger purges if needed
+        response['Cache-Tag'] = f'{project_slug}'
+        if version_slug:
+            response['Cache-Tag'] += f',{project_slug}-{version_slug}'
+
+        if hasattr(request, 'rtdheader'):
+            response['X-RTD-Project-Method'] = 'rtdheader'
+        elif hasattr(request, 'subdomain'):
+            response['X-RTD-Project-Method'] = 'subdomain'
+        elif hasattr(request, 'cname'):
+            response['X-RTD-Project-Method'] = 'cname'
+
+        if hasattr(request, 'external_domain'):
+            response['X-RTD-Version-Method'] = 'domain'
+        else:
+            response['X-RTD-Version-Method'] = 'path'
+
     def process_request(self, request):  # noqa
         if any([not settings.USE_SUBDOMAIN, 'localhost' in request.get_host(),
                 'testserver' in request.get_host()]):
@@ -144,6 +177,8 @@ def process_response(self, request, response):  # noqa
 
         hsts_header_values = []
 
+        self.add_proxito_headers(request, response)
+
         if not request.is_secure():
             # Only set the HSTS header if the request is over HTTPS
             return response
diff --git a/readthedocs/proxito/views/mixins.py b/readthedocs/proxito/views/mixins.py
--- a/readthedocs/proxito/views/mixins.py
+++ b/readthedocs/proxito/views/mixins.py
@@ -120,25 +120,7 @@ def _serve_docs_nginx(self, request, final_project, version_slug, path, download
                 filename = f'{domain}-{final_project.language}-{version_slug}.{filename_ext}'
             response['Content-Disposition'] = f'filename={filename}'
 
-        # Add debugging headers to proxito responses
-        response['X-RTD-Domain'] = request.get_host()
-        response['X-RTD-Project'] = final_project.slug
-        response['X-RTD-Version'] = version_slug
-        # Needed to strip any GET args, etc.
-        response['X-RTD-Path'] = urlparse(path).path
-        # Include the project & project-version so we can do larger purges if needed
-        response['Cache-Tag'] = f'{final_project.slug}-{version_slug},{final_project.slug}'
-        if hasattr(request, 'rtdheader'):
-            response['X-RTD-Project-Method'] = 'rtdheader'
-        elif hasattr(request, 'subdomain'):
-            response['X-RTD-Project-Method'] = 'subdomain'
-        elif hasattr(request, 'cname'):
-            response['X-RTD-Project-Method'] = 'cname'
-        if hasattr(request, 'external_domain'):
-            response['X-RTD-Version-Method'] = 'domain'
-        else:
-            response['X-RTD-Version-Method'] = 'path'
-
+        response.proxito_path = urlparse(path).path
         return response
 
     def _serve_401(self, request, project):
diff --git a/readthedocs/proxito/views/utils.py b/readthedocs/proxito/views/utils.py
--- a/readthedocs/proxito/views/utils.py
+++ b/readthedocs/proxito/views/utils.py
@@ -85,4 +85,7 @@ def _get_project_data_from_request(
     # * Subproject
     # * Translations
 
+    # Set the version slug on the request so we can log it in middleware
+    request.path_version_slug = version_slug
+
     return final_project, lang_slug, version_slug, filename


Test patch:
diff --git a/readthedocs/proxito/tests/test_headers.py b/readthedocs/proxito/tests/test_headers.py
new file mode 100644
--- /dev/null
+++ b/readthedocs/proxito/tests/test_headers.py
@@ -0,0 +1,67 @@
+import pytest
+from django.test import override_settings
+
+import django_dynamic_fixture as fixture
+
+from readthedocs.projects.models import Domain
+from .base import BaseDocServing
+
+
+@override_settings(
+    PUBLIC_DOMAIN='dev.readthedocs.io',
+    PUBLIC_DOMAIN_USES_HTTPS=True,
+)
+class ProxitoHeaderTests(BaseDocServing):
+
+    def test_redirect_headers(self):
+        r = self.client.get('', HTTP_HOST='project.dev.readthedocs.io')
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(r['X-RTD-Redirect'], 'system')
+        self.assertEqual(
+            r['Location'], 'https://project.dev.readthedocs.io/en/latest/',
+        )
+        self.assertEqual(r['Cache-Tag'], 'project')
+        self.assertEqual(r['X-RTD-Project'], 'project')
+        self.assertEqual(r['X-RTD-Project-Method'], 'subdomain')
+        self.assertEqual(r['X-RTD-Domain'], 'project.dev.readthedocs.io')
+        self.assertIsNone(r.get('X-RTD-Version'))
+        self.assertIsNone(r.get('X-RTD-Path'))
+
+    def test_serve_headers(self):
+        r = self.client.get('/en/latest/', HTTP_HOST='project.dev.readthedocs.io')
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r['Cache-Tag'], 'project,project-latest')
+        self.assertEqual(r['X-RTD-Domain'], 'project.dev.readthedocs.io')
+        self.assertEqual(r['X-RTD-Project'], 'project')
+        self.assertEqual(r['X-RTD-Project-Method'], 'subdomain')
+        self.assertEqual(r['X-RTD-Version'], 'latest')
+        self.assertEqual(r['X-RTD-version-Method'], 'path')
+        self.assertEqual(r['X-RTD-Path'], '/proxito/media/html/project/latest/index.html')
+
+    def test_404_headers(self):
+        r = self.client.get('/foo/bar.html', HTTP_HOST='project.dev.readthedocs.io')
+        self.assertEqual(r.status_code, 404)
+        self.assertEqual(r['Cache-Tag'], 'project')
+        self.assertEqual(r['X-RTD-Domain'], 'project.dev.readthedocs.io')
+        self.assertEqual(r['X-RTD-Project'], 'project')
+        self.assertEqual(r['X-RTD-Project-Method'], 'subdomain')
+        self.assertEqual(r['X-RTD-version-Method'], 'path')
+        self.assertIsNone(r.get('X-RTD-Version'))
+        self.assertIsNone(r.get('X-RTD-Path'))
+
+    def test_custom_domain_headers(self):
+        hostname = 'docs.random.com'
+        self.domain = fixture.get(
+            Domain,
+            project=self.project,
+            domain=hostname,
+        )
+        r = self.client.get("/en/latest/", HTTP_HOST=hostname)
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r['Cache-Tag'], 'project,project-latest')
+        self.assertEqual(r['X-RTD-Domain'], self.domain.domain)
+        self.assertEqual(r['X-RTD-Project'], self.project.slug)
+        self.assertEqual(r['X-RTD-Project-Method'], 'cname')
+        self.assertEqual(r['X-RTD-Version'], 'latest')
+        self.assertEqual(r['X-RTD-version-Method'], 'path')
+        self.assertEqual(r['X-RTD-Path'], '/proxito/media/html/project/latest/index.html')
diff --git a/readthedocs/proxito/tests/test_redirects.py b/readthedocs/proxito/tests/test_redirects.py
--- a/readthedocs/proxito/tests/test_redirects.py
+++ b/readthedocs/proxito/tests/test_redirects.py
@@ -129,3 +129,4 @@ def test_proper_page_on_subdomain(self):
             r['Location'],
             'https://project.dev.readthedocs.io/en/latest/test.html',
         )
+