Repository: zulip/zulip

Issue:
TypeError: argument of type 'NoneType' is not iterable
https://sentry.io/share/issue/71c35f8aca6444b4a8e1d3ab6880ef71/

```
TypeError: argument of type 'NoneType' is not iterable
(1 additional frame(s) were not displayed)
...
  File "zerver/worker/queue_processors.py", line 335, in <lambda>
    consume_func = lambda events: self.consume(events[0])
  File "zerver/worker/queue_processors.py", line 749, in consume
    do_rest_call(service.base_url, event, service_handler)
  File "zerver/lib/outgoing_webhook.py", line 325, in do_rest_call
    process_success_response(event, service_handler, response)
  File "zerver/lib/outgoing_webhook.py", line 293, in process_success_response
    success_data = service_handler.process_success(response_json)
  File "zerver/lib/outgoing_webhook.py", line 79, in process_success
    if "response_not_required" in response_json and response_json["response_not_required"]:
```


Discussion:
Hello @zulip/server-production members, this issue was labeled with the "area: production" label, so you may want to check it out!

<!-- areaLabelAddition -->

Resolving patch:
diff --git a/zerver/lib/outgoing_webhook.py b/zerver/lib/outgoing_webhook.py
--- a/zerver/lib/outgoing_webhook.py
+++ b/zerver/lib/outgoing_webhook.py
@@ -288,8 +288,10 @@ def process_success_response(
     try:
         response_json = json.loads(response.text)
     except json.JSONDecodeError:
-        fail_with_message(event, "Invalid JSON in response")
-        return
+        raise JsonableError(_("Invalid JSON in response"))
+
+    if not isinstance(response_json, dict):
+        raise JsonableError(_("Invalid response format"))
 
     success_data = service_handler.process_success(response_json)
 


Test patch:
diff --git a/zerver/tests/test_outgoing_webhook_interfaces.py b/zerver/tests/test_outgoing_webhook_interfaces.py
--- a/zerver/tests/test_outgoing_webhook_interfaces.py
+++ b/zerver/tests/test_outgoing_webhook_interfaces.py
@@ -5,6 +5,7 @@
 import requests
 
 from zerver.lib.avatar import get_gravatar_url
+from zerver.lib.exceptions import JsonableError
 from zerver.lib.message import MessageDict
 from zerver.lib.outgoing_webhook import get_service_interface_class, process_success_response
 from zerver.lib.test_classes import ZulipTestCase
@@ -47,13 +48,12 @@ def test_process_success_response(self) -> None:
         response.status_code = 200
         response.text = "unparsable text"
 
-        with mock.patch("zerver.lib.outgoing_webhook.fail_with_message") as m:
+        with self.assertRaisesRegex(JsonableError, "Invalid JSON in response"):
             process_success_response(
                 event=event,
                 service_handler=service_handler,
                 response=response,
             )
-        self.assertTrue(m.called)
 
     def test_make_request(self) -> None:
         othello = self.example_user("othello")
diff --git a/zerver/tests/test_outgoing_webhook_system.py b/zerver/tests/test_outgoing_webhook_system.py
--- a/zerver/tests/test_outgoing_webhook_system.py
+++ b/zerver/tests/test_outgoing_webhook_system.py
@@ -86,7 +86,7 @@ def test_successful_request(self) -> None:
         for service_class in [GenericOutgoingWebhookService, SlackOutgoingWebhookService]:
             handler = service_class("token", bot_user, "service")
             with mock.patch.object(handler, "session") as session:
-                session.post.return_value = ResponseMock(200)
+                session.post.return_value = ResponseMock(200, b"{}")
                 do_rest_call("", mock_event, handler)
                 session.post.assert_called_once()
 
@@ -158,7 +158,7 @@ def test_headers(self) -> None:
 
         session = service_handler.session
         with mock.patch.object(session, "send") as mock_send:
-            mock_send.return_value = ResponseMock(200)
+            mock_send.return_value = ResponseMock(200, b"{}")
             final_response = do_rest_call("https://example.com/", mock_event, service_handler)
             assert final_response is not None
 
@@ -261,6 +261,62 @@ def test_jsonable_exception(self) -> None:
         assert bot_user.bot_owner is not None
         self.assertEqual(bot_owner_notification.recipient_id, bot_user.bot_owner.recipient_id)
 
+    def test_invalid_response_format(self) -> None:
+        bot_user = self.example_user("outgoing_webhook_bot")
+        mock_event = self.mock_event(bot_user)
+        service_handler = GenericOutgoingWebhookService("token", bot_user, "service")
+
+        expect_logging_info = self.assertLogs(level="INFO")
+        expect_fail = mock.patch("zerver.lib.outgoing_webhook.fail_with_message")
+
+        with responses.RequestsMock(assert_all_requests_are_fired=True) as requests_mock:
+            # We mock the endpoint to return response with valid json which doesn't
+            # translate to a dict like is expected,
+            requests_mock.add(
+                requests_mock.POST, "https://example.zulip.com", status=200, json=True
+            )
+            with expect_logging_info, expect_fail as mock_fail:
+                do_rest_call("https://example.zulip.com", mock_event, service_handler)
+            self.assertTrue(mock_fail.called)
+            bot_owner_notification = self.get_last_message()
+            self.assertEqual(
+                bot_owner_notification.content,
+                """[A message](http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
+The outgoing webhook server attempted to send a message in Zulip, but that request resulted in the following error:
+> Invalid response format""",
+            )
+        assert bot_user.bot_owner is not None
+        self.assertEqual(bot_owner_notification.recipient_id, bot_user.bot_owner.recipient_id)
+
+    def test_invalid_json_in_response(self) -> None:
+        bot_user = self.example_user("outgoing_webhook_bot")
+        mock_event = self.mock_event(bot_user)
+        service_handler = GenericOutgoingWebhookService("token", bot_user, "service")
+
+        expect_logging_info = self.assertLogs(level="INFO")
+        expect_fail = mock.patch("zerver.lib.outgoing_webhook.fail_with_message")
+
+        with responses.RequestsMock(assert_all_requests_are_fired=True) as requests_mock:
+            # We mock the endpoint to return response with a body which isn't valid json.
+            requests_mock.add(
+                requests_mock.POST,
+                "https://example.zulip.com",
+                status=200,
+                body="this isn't valid json",
+            )
+            with expect_logging_info, expect_fail as mock_fail:
+                do_rest_call("https://example.zulip.com", mock_event, service_handler)
+            self.assertTrue(mock_fail.called)
+            bot_owner_notification = self.get_last_message()
+            self.assertEqual(
+                bot_owner_notification.content,
+                """[A message](http://zulip.testserver/#narrow/stream/999-Verona/topic/Foo/near/) to your bot @_**Outgoing Webhook** triggered an outgoing webhook.
+The outgoing webhook server attempted to send a message in Zulip, but that request resulted in the following error:
+> Invalid JSON in response""",
+            )
+        assert bot_user.bot_owner is not None
+        self.assertEqual(bot_owner_notification.recipient_id, bot_user.bot_owner.recipient_id)
+
 
 class TestOutgoingWebhookMessaging(ZulipTestCase):
     def create_outgoing_bot(self, bot_owner: UserProfile) -> UserProfile:
@@ -305,7 +361,7 @@ def test_multiple_services(self) -> None:
 
         session = mock.Mock(spec=requests.Session)
         session.headers = {}
-        session.post.return_value = ResponseMock(200)
+        session.post.return_value = ResponseMock(200, b"{}")
         with mock.patch("zerver.lib.outgoing_webhook.Session") as sessionmaker:
             sessionmaker.return_value = session
             self.send_personal_message(