Repository: wagtail/wagtail

Issue:
Parsing cache_until field fails on Twitter embeds
Reported on Slack #support: Trying to embed a twitter link within a RichField paragraph triggers a 500 error.

```
ERROR 2021-08-08 19:41:11,207 log 2288 140316583144768 Internal Server Error: /cms/embeds/chooser/upload/  
...
...
  File "/home/asdf/asdffff/venv/lib/python3.7/site-packages/wagtail/embeds/finders/oembed.py", line 92, in find_embed
    result['cache_until'] = timezone.now() + timedelta(seconds=cache_age)
TypeError: unsupported type for timedelta seconds component: str
```

The response returned by Twitter's oembed endpoint is:
```
{'url': 'https://twitter.com/elonmusk/status/1423659261452709893', 'author_name': 'Elon Musk', 'author_url': 'https://twitter.com/elonmusk', 'html': '<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Starship Fully Stacked <a href="https://t.co/Fs88RNsmfH">pic.twitter.com/Fs88RNsmfH</a></p>&mdash; Elon Musk (@elonmusk) <a href="https://twitter.com/elonmusk/status/1423659261452709893?ref_src=twsrc%5Etfw">August 6, 2021</a></blockquote>\n<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>\n', 'width': 550, 'height': None, 'type': 'rich', 'cache_age': '3153600000', 'provider_name': 'Twitter', 'provider_url': 'https://twitter.com', 'version': '1.0'}
```

so it looks like we aren't handling `cache_age` being returned as a string rather than an int.

Checking `cache_age` was added in #7279, so this is most likely a 2.14 regression.


Discussion:
Should Wagtail be coercing all these inputs into a stable type for consumers? 

Currently it also just passes through the rest of the arguments as whatever types they come from from the upstream API. Unfortunately the [oembed spec](https://oembed.com) doesn't mention anything about what type fields should be.

Resolving patch:
diff --git a/wagtail/embeds/finders/oembed.py b/wagtail/embeds/finders/oembed.py
--- a/wagtail/embeds/finders/oembed.py
+++ b/wagtail/embeds/finders/oembed.py
@@ -87,8 +87,11 @@ def find_embed(self, url, max_width=None, max_height=None):
             'html': html,
         }
 
-        cache_age = oembed.get('cache_age')
-        if cache_age is not None:
+        try:
+            cache_age = int(oembed['cache_age'])
+        except (KeyError, TypeError, ValueError):
+            pass
+        else:
             result['cache_until'] = timezone.now() + timedelta(seconds=cache_age)
 
         return result


Test patch:
diff --git a/wagtail/embeds/tests/test_embeds.py b/wagtail/embeds/tests/test_embeds.py
--- a/wagtail/embeds/tests/test_embeds.py
+++ b/wagtail/embeds/tests/test_embeds.py
@@ -490,6 +490,37 @@ def test_oembed_cache_until(self, loads, urlopen, now):
             'cache_until': make_aware(datetime.datetime(2001, 2, 3, hour=1))
         })
 
+    @patch('django.utils.timezone.now')
+    @patch('urllib.request.urlopen')
+    @patch('json.loads')
+    def test_oembed_cache_until_as_string(self, loads, urlopen, now):
+        urlopen.return_value = self.dummy_response
+        loads.return_value = {
+            'type': 'something',
+            'url': 'http://www.example.com',
+            'title': 'test_title',
+            'author_name': 'test_author',
+            'provider_name': 'test_provider_name',
+            'thumbnail_url': 'test_thumbail_url',
+            'width': 'test_width',
+            'height': 'test_height',
+            'html': 'test_html',
+            'cache_age': '3600'
+        }
+        now.return_value = make_aware(datetime.datetime(2001, 2, 3))
+        result = OEmbedFinder().find_embed("http://www.youtube.com/watch/")
+        self.assertEqual(result, {
+            'type': 'something',
+            'title': 'test_title',
+            'author_name': 'test_author',
+            'provider_name': 'test_provider_name',
+            'thumbnail_url': 'test_thumbail_url',
+            'width': 'test_width',
+            'height': 'test_height',
+            'html': 'test_html',
+            'cache_until': make_aware(datetime.datetime(2001, 2, 3, hour=1))
+        })
+
     def test_oembed_accepts_known_provider(self):
         finder = OEmbedFinder(providers=[oembed_providers.youtube])
         self.assertTrue(finder.accept("http://www.youtube.com/watch/"))