Repository: readthedocs/readthedocs.org

Issue:
Embed API: gets confused with spaces in the URL
Reading #8283 I found that Embed API gets confused with this link: https://github.com/readthedocs/readthedocs.org/pull/8283/files#diff-30a1066ce8ef6760deed6b1dcb492eda05b8dd6cf71dadddab6c797c874be6edR121

In the JS console I see https://readthedocs.org/api/v2/embed/?url=https%3A%2F%2Fipywidgets.readthedocs.io%2Fen%2F7.6.3%2Fexamples%2FWidget%2520List.html%23IntSlider which fails to retrieve with the error:

```
Can't find content for section: doc=examples/Widget%20List path=examples/Widget%20List.html section=IntSlider
```

However, changing it to https://readthedocs.org/api/v2/embed/?url=https://ipywidgets.readthedocs.io/en/7.6.3/examples/Widget%20List.html%23IntSlider it does work and _it does return content_.


Discussion:
Found the issue! 💪🏼 

Encoded whitespaces on filenames break when they are retrieved from the S3 storage. The following code works fine,

```python
file_path = 'html/ipywidgets/7.6.3/examples/Widget List.html'
build_media_storage.open(file_path)
```

but encoding the ` ` as `%20` does not:

```python
file_path = 'html/ipywidgets/7.6.3/examples/Widget%20List.html'
build_media_storage.open(file_path)
```

So, the solution here is to _decode_ the `file_path` before passing it to the S3 storage backend.

This happens at https://github.com/readthedocs/readthedocs.org/blob/a8e88277353460269873fa7e38789c8c7d2e4dee/readthedocs/embed/v3/views.py#L99

Resolving patch:
diff --git a/readthedocs/embed/v3/views.py b/readthedocs/embed/v3/views.py
--- a/readthedocs/embed/v3/views.py
+++ b/readthedocs/embed/v3/views.py
@@ -1,6 +1,7 @@
 """Views for the EmbedAPI v3 app."""
 
 import re
+import urllib.parse
 from urllib.parse import urlparse
 
 import requests
@@ -90,11 +91,16 @@ def _get_page_content_from_storage(self, project, version_slug, filename):
             include_file=False,
             version_type=version.type,
         )
+
+        # Decode encoded URLs (e.g. convert %20 into a whitespace)
+        filename = urllib.parse.unquote(filename)
+
         relative_filename = filename.lstrip("/")
         file_path = build_media_storage.join(
             storage_path,
             relative_filename,
         )
+
         try:
             with build_media_storage.open(file_path) as fd:  # pylint: disable=invalid-name
                 return fd.read()


Test patch:
diff --git a/readthedocs/embed/v3/tests/test_internal_pages.py b/readthedocs/embed/v3/tests/test_internal_pages.py
--- a/readthedocs/embed/v3/tests/test_internal_pages.py
+++ b/readthedocs/embed/v3/tests/test_internal_pages.py
@@ -78,3 +78,20 @@ def test_default_main_section(self, storage_exists, storage_open, app, client):
             'content': content,
             'external': False,
         }
+
+    @pytest.mark.sphinx("html", srcdir=srcdir, freshenv=False)
+    @mock.patch("readthedocs.embed.v3.views.build_media_storage.open")
+    @mock.patch("readthedocs.embed.v3.views.build_media_storage.exists")
+    def test_s3_storage_decoded_filename(
+        self, storage_exists, storage_open, app, client
+    ):
+        storage_exists.return_value = True
+        storage_open.side_effect = self._mock_open('<div id="section">content</div>')
+
+        params = {
+            "url": "https://project.readthedocs.io/en/latest/My%20Spaced%20File.html#section",
+        }
+        response = client.get(self.api_url, params)
+        assert response.status_code == 200
+
+        storage_open.assert_called_once_with("html/project/latest/My Spaced File.html")