Repository: jordaneremieff/mangum

Issue:
Bug - handler.body returns a str resulting in TypeError: a bytes-like object is required, not 'str'
First this bug was introduced post 0.11.0 when the handlers were refactored.

**Traceback**
```
Exception in 'http' protocol.
--
Traceback (most recent call last):
File "/function/src/mangum/mangum/protocols/http.py", line 81, in run
await app(self.request.scope, self.receive, self.send)
File "/usr/local/lib/python3.8/dist-packages/django/core/handlers/asgi.py", line 149, in __call__
body_file = await self.read_body(receive)
File "/usr/local/lib/python3.8/dist-packages/django/core/handlers/asgi.py", line 181, in read_body
body_file.write(message['body'])
File "/usr/lib/python3.8/tempfile.py", line 896, in write
rv = file.write(s)
TypeError: a bytes-like object is required, not 'str'
```

---

I was able to trace the issue down to the `def body()` method in the handler classes. Adding `assert type(handler.body) == bytes` to the `_basic` tests will reveal the error.

```python
        handler = AwsAlb(example_event, example_context)
>       assert type(handler.body) == bytes
E       AssertionError: assert <class 'str'> == <class 'bytes'>
E         +<class 'str'>
E         -<class 'bytes'>
```

In 0.11.0 the body was guaranteed to be bytes because of the following snippet:
```python
is_binary = event.get("isBase64Encoded", False)
initial_body = event.get("body") or b""
if is_binary:
    initial_body = base64.b64decode(initial_body)
elif not isinstance(initial_body, bytes):  # forces strings to be bytes.
     initial_body = initial_body.encode()
```

For most handlers the refactored code looks like this:
```python
@property
def body(self) -> bytes:
    body = self.trigger_event.get("body", b"")
    if self.trigger_event.get("isBase64Encoded", False):
        body = base64.b64decode(body)
    return body
```

If `self.trigger_event` has a `body` key than the body returned will be whatever the type of the value is, which might not be bytes.

The solution I believe is to add the code from 0.11.0 to the `body` function.:
```python
if not isinstance(body, bytes):
    body = body.encode()
```

I'll work on updating the tests and putting a PR with the fix if the above seems correct. 

Discussion:
jordaneremieff: Hi @nathanglover, thanks for the PR!

Yep, you're right. Before the recent refactoring this was handled like this:

```python
is_binary = event.get("isBase64Encoded", False)
body = event.get("body") or b""
if is_binary:
    body = base64.b64decode(body)
elif not isinstance(body, bytes):
    body = body.encode()
```
The intent of the `or` in `body = event.get("body") or b""` was for handling this case. Could you update your PR to do `body = self.trigger_event.get("body") or b""` to replace the additional if statement? 

Then I think this is good to merge, thank you.
nathanglover: @jordaneremieff You're welcome. Removed the extra if and added the `or` as you asked.  That way is a bit cleaner.. 

Patch:
diff --git a/mangum/handlers/aws_alb.py b/mangum/handlers/aws_alb.py
index b2c6470..33e0968 100644
--- a/mangum/handlers/aws_alb.py
+++ b/mangum/handlers/aws_alb.py
@@ -57,9 +57,13 @@ class AwsAlb(AbstractHandler):
 
     @property
     def body(self) -> bytes:
-        body = self.trigger_event.get("body", b"")
+        body = self.trigger_event.get("body", b"") or b""
+
         if self.trigger_event.get("isBase64Encoded", False):
-            body = base64.b64decode(body)
+            return base64.b64decode(body)
+        if not isinstance(body, bytes):
+            body = body.encode()
+
         return body
 
     def transform_response(self, response: Response) -> Dict[str, Any]:
diff --git a/mangum/handlers/aws_api_gateway.py b/mangum/handlers/aws_api_gateway.py
index 6267ffa..2e239d4 100644
--- a/mangum/handlers/aws_api_gateway.py
+++ b/mangum/handlers/aws_api_gateway.py
@@ -95,9 +95,13 @@ class AwsApiGateway(AbstractHandler):
 
     @property
     def body(self) -> bytes:
-        body = self.trigger_event.get("body", b"")
+        body = self.trigger_event.get("body", b"") or b""
+
         if self.trigger_event.get("isBase64Encoded", False):
-            body = base64.b64decode(body)
+            return base64.b64decode(body)
+        if not isinstance(body, bytes):
+            body = body.encode()
+
         return body
 
     def transform_response(self, response: Response) -> Dict[str, Any]:
diff --git a/mangum/handlers/aws_cf_lambda_at_edge.py b/mangum/handlers/aws_cf_lambda_at_edge.py
index 85013bd..b43132c 100644
--- a/mangum/handlers/aws_cf_lambda_at_edge.py
+++ b/mangum/handlers/aws_cf_lambda_at_edge.py
@@ -55,9 +55,13 @@ class AwsCfLambdaAtEdge(AbstractHandler):
     @property
     def body(self) -> bytes:
         request = self.trigger_event["Records"][0]["cf"]["request"]
-        body = request.get("body", {}).get("data", None)
+        body = request.get("body", {}).get("data", None) or b""
+
         if request.get("body", {}).get("encoding", "") == "base64":
-            body = base64.b64decode(body)
+            return base64.b64decode(body)
+        if not isinstance(body, bytes):
+            body = body.encode()
+
         return body
 
     def transform_response(self, response: Response) -> Dict[str, Any]:
diff --git a/mangum/handlers/aws_http_gateway.py b/mangum/handlers/aws_http_gateway.py
index 6ff783c..513c494 100644
--- a/mangum/handlers/aws_http_gateway.py
+++ b/mangum/handlers/aws_http_gateway.py
@@ -102,9 +102,13 @@ class AwsHttpGateway(AbstractHandler):
 
     @property
     def body(self) -> bytes:
-        body = self.trigger_event.get("body", b"")
+        body = self.trigger_event.get("body", b"") or b""
+
         if self.trigger_event.get("isBase64Encoded", False):
-            body = base64.b64decode(body)
+            return base64.b64decode(body)
+        if not isinstance(body, bytes):
+            body = body.encode()
+
         return body
 
     def transform_response(self, response: Response) -> Dict[str, Any]:


Test patch:
diff --git a/tests/handlers/test_aws_alb.py b/tests/handlers/test_aws_alb.py
index 216d8a8..d922c37 100644
--- a/tests/handlers/test_aws_alb.py
+++ b/tests/handlers/test_aws_alb.py
@@ -74,6 +74,8 @@ def test_aws_alb_basic():
 
     example_context = {}
     handler = AwsAlb(example_event, example_context)
+
+    assert type(handler.body) == bytes
     assert handler.request.scope == {
         "asgi": {"version": "3.0"},
         "aws.context": {},
@@ -119,7 +121,15 @@ def test_aws_alb_basic():
     "query_string,scope_body",
     [
         ("GET", "/hello/world", None, None, False, b"", None),
-        ("POST", "/", {"name": ["me"]}, None, False, b"name=me", None),
+        (
+            "POST",
+            "/",
+            {"name": ["me"]},
+            "field1=value1&field2=value2",
+            False,
+            b"name=me",
+            b"field1=value1&field2=value2",
+        ),
         (
             "GET",
             "/my/resource",
@@ -210,7 +220,10 @@ def test_aws_alb_scope_real(
         "type": "http",
     }
 
-    assert handler.body == scope_body
+    if handler.body:
+        assert handler.body == scope_body
+    else:
+        assert handler.body == b""
 
 
 def test_aws_alb_set_cookies() -> None:
diff --git a/tests/handlers/test_aws_api_gateway.py b/tests/handlers/test_aws_api_gateway.py
index bc07d08..e35bf4e 100644
--- a/tests/handlers/test_aws_api_gateway.py
+++ b/tests/handlers/test_aws_api_gateway.py
@@ -96,6 +96,8 @@ def test_aws_api_gateway_scope_basic():
     }
     example_context = {}
     handler = AwsApiGateway(example_event, example_context)
+
+    assert type(handler.body) == bytes
     assert handler.request.scope == {
         "asgi": {"version": "3.0"},
         "aws.context": {},
@@ -128,7 +130,15 @@ def test_aws_api_gateway_scope_basic():
     "query_string,scope_body",
     [
         ("GET", "/hello/world", None, None, False, b"", None),
-        ("POST", "/", {"name": ["me"]}, None, False, b"name=me", None),
+        (
+            "POST",
+            "/",
+            {"name": ["me"]},
+            "field1=value1&field2=value2",
+            False,
+            b"name=me",
+            b"field1=value1&field2=value2",
+        ),
         (
             "GET",
             "/my/resource",
@@ -218,7 +228,10 @@ def test_aws_api_gateway_scope_real(
         "type": "http",
     }
 
-    assert handler.body == scope_body
+    if handler.body:
+        assert handler.body == scope_body
+    else:
+        assert handler.body == b""
 
 
 @pytest.mark.parametrize(
diff --git a/tests/handlers/test_aws_cf_lambda_at_edge.py b/tests/handlers/test_aws_cf_lambda_at_edge.py
index 650e4e2..4692e79 100644
--- a/tests/handlers/test_aws_cf_lambda_at_edge.py
+++ b/tests/handlers/test_aws_cf_lambda_at_edge.py
@@ -136,6 +136,7 @@ def test_aws_cf_lambda_at_edge_scope_basic():
     example_context = {}
     handler = AwsCfLambdaAtEdge(example_event, example_context)
 
+    assert type(handler.body) == bytes
     assert handler.request.scope == {
         "asgi": {"version": "3.0"},
         "aws.context": {},
@@ -169,7 +170,15 @@ def test_aws_cf_lambda_at_edge_scope_basic():
     "body_base64_encoded,query_string,scope_body",
     [
         ("GET", "/hello/world", None, None, False, b"", None),
-        ("POST", "/", {"name": ["me"]}, None, False, b"name=me", None),
+        (
+            "POST",
+            "/",
+            {"name": ["me"]},
+            "field1=value1&field2=value2",
+            False,
+            b"name=me",
+            b"field1=value1&field2=value2",
+        ),
         (
             "GET",
             "/my/resource",
@@ -241,7 +250,10 @@ def test_aws_api_gateway_scope_real(
         "type": "http",
     }
 
-    assert handler.body == scope_body
+    if handler.body:
+        assert handler.body == scope_body
+    else:
+        assert handler.body == b""
 
 
 @pytest.mark.parametrize(
diff --git a/tests/handlers/test_aws_http_gateway.py b/tests/handlers/test_aws_http_gateway.py
index 56b9115..4e87f9d 100644
--- a/tests/handlers/test_aws_http_gateway.py
+++ b/tests/handlers/test_aws_http_gateway.py
@@ -195,6 +195,8 @@ def test_aws_http_gateway_scope_basic_v1():
     }
     example_context = {}
     handler = AwsHttpGateway(example_event, example_context)
+
+    assert type(handler.body) == bytes
     assert handler.request.scope == {
         "asgi": {"version": "3.0"},
         "aws.context": {},
@@ -297,6 +299,8 @@ def test_aws_http_gateway_scope_basic_v2():
     }
     example_context = {}
     handler = AwsHttpGateway(example_event, example_context)
+
+    assert type(handler.body) == bytes
     assert handler.request.scope == {
         "asgi": {"version": "3.0"},
         "aws.context": {},
@@ -396,7 +400,10 @@ def test_aws_http_gateway_scope_real_v1(
         "type": "http",
     }
 
-    assert handler.body == scope_body
+    if handler.body:
+        assert handler.body == scope_body
+    else:
+        assert handler.body == b""
 
 
 @pytest.mark.parametrize(
@@ -461,7 +468,10 @@ def test_aws_http_gateway_scope_real_v2(
         "type": "http",
     }
 
-    assert handler.body == scope_body
+    if handler.body:
+        assert handler.body == scope_body
+    else:
+        assert handler.body == b""
 
 
 @pytest.mark.parametrize(