Repository: zulip/zulip

Issue:
Webhooks posting as form/multipart causes 500s
Hellosign [posts their callback as form/multipart](https://app.hellosign.com/api/eventsAndCallbacksWalkthrough), which Django only permits to be read once. Attempts to access `request.body` after the initial read throw "django.http.request.RawPostDataException: You cannot access body after reading from request's data stream".

You can trivially reproduce this locally, by provisioning a webhook API key, then invoking:

    curl -F json=foobar -XPOST \
      http://localhost:9991/api/v1/external/hellosign?api_key=key

Which results in:

```
2020-02-07 22:57:28.177 ERR  [] Traceback (most recent call last):
  File "/home/chris/projects/zulip/zerver/decorator.py", line 356, in _wrapped_func_arguments
    return view_func(request, user_profile, *args, **kwargs)
  File "/home/chris/projects/zulip/zerver/lib/request.py", line 306, in _wrapped_view_func
    request_body = request.body
  File "/srv/zulip-py3-venv/lib/python3.7/site-packages/django/http/request.py", line 265, in body
    raise RawPostDataException("You cannot access body after reading from request's data stream")
django.http.request.RawPostDataException: You cannot access body after reading from request's data stream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/srv/zulip-py3-venv/lib/python3.7/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/srv/zulip-py3-venv/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/chris/projects/zulip/zerver/lib/request.py", line 372, in _wrapped_view_func
    return view_func(request, *args, **kwargs)
  File "/home/chris/projects/zulip/zerver/decorator.py", line 369, in _wrapped_func_arguments
    log_exception_to_webhook_logger(**kwargs)
  File "/home/chris/projects/zulip/zerver/decorator.py", line 285, in log_exception_to_webhook_logger
    payload = request.body
  File "/srv/zulip-py3-venv/lib/python3.7/site-packages/django/http/request.py", line 265, in body
    raise RawPostDataException("You cannot access body after reading from request's data stream")
django.http.request.RawPostDataException: You cannot access body after reading from request's data stream
```


Discussion:
I was able to fixup the HelloSign webhook by changing the decorator arguments:

```diff
diff --git a/zerver/webhooks/hellosign/view.py b/zerver/webhooks/hellosign/view.py
index 7a89208e3..5a6ea6814 100644
--- a/zerver/webhooks/hellosign/view.py
+++ b/zerver/webhooks/hellosign/view.py
@@ -54,8 +54,10 @@ def get_recipients_text(recipients: List[str]) -> str:
 @api_key_only_webhook_view('HelloSign')
 @has_request_variables
 def api_hellosign_webhook(request: HttpRequest, user_profile: UserProfile,
-                          payload: Dict[str, Dict[str, Any]]=REQ(argument_type='body')) -> HttpResponse:
-    body = get_message_body(payload)
-    topic = payload['signature_request']['title']
-    check_send_webhook_message(request, user_profile, topic, body)
+                          payloads: List[Dict[str, Dict[str, Any]]]=REQ(aliases=['json'])) -> HttpResponse:
+    handleable_payloads = [p for p in payloads if "signature_request" in p]
+    for payload in handleable_payloads:
+        body = get_message_body(payload)
+        topic = payload['signature_request']['title']
+        check_send_webhook_message(request, user_profile, topic, body)
     return json_success()
```

Resolving patch:
diff --git a/zerver/webhooks/hellosign/view.py b/zerver/webhooks/hellosign/view.py
--- a/zerver/webhooks/hellosign/view.py
+++ b/zerver/webhooks/hellosign/view.py
@@ -7,6 +7,7 @@
 from zerver.lib.response import json_success
 from zerver.lib.webhooks.common import check_send_webhook_message
 from zerver.models import UserProfile
+import ujson
 
 IS_AWAITING_SIGNATURE = "is awaiting the signature of {awaiting_recipients}"
 WAS_JUST_SIGNED_BY = "was just signed by {signed_recipients}"
@@ -54,8 +55,11 @@ def get_recipients_text(recipients: List[str]) -> str:
 @api_key_only_webhook_view('HelloSign')
 @has_request_variables
 def api_hellosign_webhook(request: HttpRequest, user_profile: UserProfile,
-                          payload: Dict[str, Dict[str, Any]]=REQ(argument_type='body')) -> HttpResponse:
-    body = get_message_body(payload)
-    topic = payload['signature_request']['title']
-    check_send_webhook_message(request, user_profile, topic, body)
-    return json_success()
+                          payload: Dict[str, Dict[str, Any]]=REQ(
+                              whence='json', converter=ujson.loads)) -> HttpResponse:
+    if "signature_request" in payload:
+        body = get_message_body(payload)
+        topic = payload['signature_request']['title']
+        check_send_webhook_message(request, user_profile, topic, body)
+
+    return json_success({"msg": "Hello API Event Received"})


Test patch:
diff --git a/zerver/webhooks/hellosign/tests.py b/zerver/webhooks/hellosign/tests.py
--- a/zerver/webhooks/hellosign/tests.py
+++ b/zerver/webhooks/hellosign/tests.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 from zerver.lib.test_classes import WebhookTestCase
-
+from typing import Dict
 
 class HelloSignHookTests(WebhookTestCase):
     STREAM_NAME = 'hellosign'
@@ -12,21 +12,21 @@ def test_signatures_message(self) -> None:
         expected_message = ("The `NDA with Acme Co.` document is awaiting the signature of "
                             "Jack, and was just signed by Jill.")
         self.send_and_test_stream_message('signatures', expected_topic, expected_message,
-                                          content_type="application/x-www-form-urlencoded")
+                                          content_type=None)
 
     def test_signatures_message_signed_by_one(self) -> None:
         expected_topic = "NDA with Acme Co."
         expected_message = ("The `NDA with Acme Co.` document was just signed by Jill.")
         self.send_and_test_stream_message('signatures_signed_by_one_signatory',
                                           expected_topic, expected_message,
-                                          content_type="application/x-www-form-urlencoded")
+                                          content_type=None)
 
     def test_signatures_message_with_four_signatories(self) -> None:
         expected_topic = "Signature doc"
         expected_message = ("The `Signature doc` document is awaiting the signature of "
                             "Eeshan Garg, John Smith, Jane Doe, and Stephen Strange.")
         self.send_and_test_stream_message('signatures_with_four_signatories', expected_topic, expected_message,
-                                          content_type="application/x-www-form-urlencoded")
+                                          content_type=None)
 
     def test_signatures_message_with_own_subject(self) -> None:
         expected_topic = "Our own subject."
@@ -34,7 +34,7 @@ def test_signatures_message_with_own_subject(self) -> None:
         expected_message = ("The `NDA with Acme Co.` document is awaiting the signature of "
                             "Jack, and was just signed by Jill.")
         self.send_and_test_stream_message('signatures_with_own_subject', expected_topic, expected_message,
-                                          content_type="application/x-www-form-urlencoded", topic=expected_topic)
+                                          content_type=None, topic=expected_topic)
 
-    def get_body(self, fixture_name: str) -> str:
-        return self.webhook_fixture_data("hellosign", fixture_name, file_type="json")
+    def get_body(self, fixture_name: str) -> Dict[str, str]:
+        return {"json": self.webhook_fixture_data("hellosign", fixture_name, file_type="json")}