From 8abfbbbc715d84ada59c990bd28158404b865763 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Fri, 25 Jul 2025 09:40:23 -0700 Subject: [PATCH] ActivityPub._get: check that we get an object (dict) back fixes https://console.cloud.google.com/errors/detail/CMKg3srU6Mbs2wE;locations=global;time=P30D?project=bridgy-federated&inv=1&invt=Ab3tnA --- activitypub.py | 4 ++++ common.py | 4 ++++ tests/test_activitypub.py | 26 ++++++++++++++++++-------- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/activitypub.py b/activitypub.py index 81e31492..32747015 100644 --- a/activitypub.py +++ b/activitypub.py @@ -438,6 +438,10 @@ class ActivityPub(User, Protocol): obj = resp.json() except requests.JSONDecodeError: _error("Couldn't decode as JSON") + if not isinstance(obj, dict): + logger.warning(f'Got non-object: {obj}') + return resp, None + cls._hydrate(obj) return resp, obj diff --git a/common.py b/common.py index fb8e45ce..01670cb2 100644 --- a/common.py +++ b/common.py @@ -212,6 +212,10 @@ def content_type(resp): """Returns a :class:`requests.Response`'s Content-Type, without charset suffix.""" type = resp.headers.get('Content-Type') if type: + # TODO: don't remove profile + # right now, when we remove it, and don't use it to compare against eg + # as2.CONTENT_TYPE_LD, we end up accepting non-AS2 JSON-LD, eg: + # Content-Type: application/ld+json; charset=UTF-8 return type.split(';')[0] diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 5acdc3f9..aa4eb151 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -2864,24 +2864,34 @@ class ActivityPubUtilsTest(TestCase): mock_post.assert_called_once() self.assertEqual(302, resp.status_code) - @patch('requests.get') + @patch('requests.get', return_value=AS2) def test_fetch_direct(self, mock_get): - mock_get.return_value = AS2 obj = Object(id='http://orig') - ActivityPub.fetch(obj) + self.assertTrue(ActivityPub.fetch(obj)) self.assertEqual(AS2_OBJ, obj.as2) mock_get.assert_has_calls(( self.as2_req('http://orig'), )) + @patch('requests.get') + def test_fetch_direct_list(self, mock_get): + mock_get.return_value = self.as2_resp([AS2_OBJ]) + obj = Object(id='http://orig') + self.assertFalse(ActivityPub.fetch(obj)) + self.assertIsNone(obj.as2) + + mock_get.assert_has_calls(( + self.as2_req('http://orig'), + )) + @patch('requests.get') def test_fetch_direct_ld_content_type(self, mock_get): mock_get.return_value = requests_response(AS2_OBJ, headers={ 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', }) obj = Object(id='http://orig') - ActivityPub.fetch(obj) + self.assertTrue(ActivityPub.fetch(obj)) self.assertEqual(AS2_OBJ, obj.as2) mock_get.assert_has_calls(( @@ -2892,7 +2902,7 @@ class ActivityPubUtilsTest(TestCase): def test_fetch_via_html(self, mock_get): mock_get.side_effect = [HTML_WITH_AS2, AS2] obj = Object(id='http://orig') - ActivityPub.fetch(obj) + self.assertTrue(ActivityPub.fetch(obj)) self.assertEqual(AS2_OBJ, obj.as2) mock_get.assert_has_calls(( @@ -2956,7 +2966,7 @@ class ActivityPubUtilsTest(TestCase): mock_get.return_value = self.as2_resp(event_article) obj = Object(id='http://orig') - ActivityPub.fetch(obj) + self.assertTrue(ActivityPub.fetch(obj)) self.assertEqual(event_article, obj.as2) @patch('requests.get') @@ -2969,7 +2979,7 @@ class ActivityPubUtilsTest(TestCase): mock_get.side_effect = [self.as2_resp(actor), self.as2_resp(featured)] obj = Object(id='http://orig') - ActivityPub.fetch(obj) + self.assertTrue(ActivityPub.fetch(obj)) self.assertEqual({**actor, 'featured': {'foo': 'bar'}}, obj.as2) mock_get.assert_has_calls(( @@ -2986,7 +2996,7 @@ class ActivityPubUtilsTest(TestCase): mock_get.return_value = self.as2_resp(actor) obj = Object(id='http://orig') - ActivityPub.fetch(obj) + self.assertTrue(ActivityPub.fetch(obj)) self.assertEqual(actor, obj.as2) mock_get.assert_called_once()