Repository: aws/chalice

Issue:
Binary data is returned base64 encoded 
I am attempting to use Chalice to return a PDF file.  It works fine with `chalice local`, but when live, all the content returned is base64 encoded instead of returning raw bytes.  I believe I am following all of the guidelines to get this to work: I am setting the Content-Type header, that type is added to binary_types, and the data I am providing is of type 'bytes'.

I have built a minimal test case, using a PDF file in /vendors/test.pdf:

* app.py
```python
from chalice import Chalice, Response, NotFoundError

from pprint import pprint

app = Chalice(app_name='pdf-download-test-case')

app.api.binary_types.append('application/pdf')

@app.route('/')
def index():
    with open('./test.pdf', 'rb') as pdf:
        content = pdf.read()

    pprint(type(content))
    pprint(app.api.binary_types)

    return Response(body=content,
                    status_code=200,
                    headers={'Content-Type': 'application/pdf'})
```

* output (verifying my data is the correct type, and the MIME type was added)
```
2020-08-28 13:46:13.685000 fea9ad <class 'bytes'>
2020-08-28 13:46:13.685000 fea9ad ['application/octet-stream',
2020-08-28 13:46:13.685000 fea9ad 'application/x-tar',
2020-08-28 13:46:13.685000 fea9ad 'application/zip',
2020-08-28 13:46:13.685000 fea9ad 'audio/basic',
2020-08-28 13:46:13.685000 fea9ad 'audio/ogg',
2020-08-28 13:46:13.685000 fea9ad 'audio/mp4',
2020-08-28 13:46:13.685000 fea9ad 'audio/mpeg',
2020-08-28 13:46:13.685000 fea9ad 'audio/wav',
2020-08-28 13:46:13.685000 fea9ad 'audio/webm',
2020-08-28 13:46:13.685000 fea9ad 'image/png',
2020-08-28 13:46:13.685000 fea9ad 'image/jpg',
2020-08-28 13:46:13.685000 fea9ad 'image/jpeg',
2020-08-28 13:46:13.685000 fea9ad 'image/gif',
2020-08-28 13:46:13.685000 fea9ad 'video/ogg',
2020-08-28 13:46:13.685000 fea9ad 'video/mpeg',
2020-08-28 13:46:13.685000 fea9ad 'video/webm',
2020-08-28 13:46:13.685000 fea9ad 'application/pdf']
```

Is this a bug, or is there configuration that I am missing?

Discussion:

Patch:
diff --git a/.changes/next-release/51581685415-enhancement-Binary-64168.json b/.changes/next-release/51581685415-enhancement-Binary-64168.json
new file mode 100644
index 0000000..a40e212
--- /dev/null
+++ b/.changes/next-release/51581685415-enhancement-Binary-64168.json
@@ -0,0 +1,5 @@
+{
+  "type": "enhancement",
+  "category": "Binary",
+  "description": "Support returning native python types when using `*/*` for binary types (#1501)"
+}
diff --git a/chalice/app.py b/chalice/app.py
index b75bf09..d2bfe20 100644
--- a/chalice/app.py
+++ b/chalice/app.py
@@ -494,12 +494,17 @@ class Response(object):
         body = response_dict['body']
 
         if _matches_content_type(content_type, binary_types):
-            if _matches_content_type(content_type, ['application/json']):
+            if _matches_content_type(content_type, ['application/json']) or \
+                    not content_type:
                 # There's a special case when a user configures
                 # ``application/json`` as a binary type.  The default
                 # json serialization results in a string type, but for binary
                 # content types we need a type bytes().  So we need to special
                 # case this scenario and encode the JSON body to bytes().
+                #
+                # If a user does not provide a content type header, which can
+                # happen if they return a python type instead of a ``Response``
+                # type, then we assume the content is application/json.
                 body = body if isinstance(body, bytes) \
                     else body.encode('utf-8')
             body = self._base64encode(body)


Test patch:
diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py
index 9853101..bdec020 100644
--- a/tests/unit/test_app.py
+++ b/tests/unit/test_app.py
@@ -415,6 +415,44 @@ def test_can_encode_binary_json(sample_app):
     assert encoded_response['body'] == 'eyJmb28iOiJiYXIifQ=='
 
 
+def test_wildcard_accepts_with_native_python_types_serializes_json(
+        sample_app, create_event):
+    sample_app.api.binary_types = ['*/*']
+
+    @sample_app.route('/py-dict')
+    def py_dict():
+        return {'foo': 'bar'}
+
+    event = create_event('/py-dict', 'GET', {})
+    event['headers']['Accept'] = '*/*'
+    response = sample_app(event, context=None)
+    # In this case, they've return a native python dict type, which should
+    # be serialized to JSON and returned back to the user as JSON.  Because
+    # we also have ``*/*`` as a binary type, we'll return the response
+    # as a binary response type.
+    assert base64.b64decode(response['body']) == b'{"foo":"bar"}'
+    assert response['isBase64Encoded']
+
+
+def test_wildcard_accepts_with_response_class(
+        sample_app, create_event):
+    sample_app.api.binary_types = ['*/*']
+
+    @sample_app.route('/py-dict')
+    def py_dict():
+        return Response(body=json.dumps({'foo': 'bar'}).encode('utf-8'),
+                        headers={'Content-Type': 'application/json'},
+                        status_code=200)
+
+    event = create_event('/py-dict', 'GET', {})
+    event['headers']['Accept'] = '*/*'
+    response = sample_app(event, context=None)
+    # Because our binary types is '*/*' we should be returning this
+    # content as binary.
+    assert base64.b64decode(response['body']) == b'{"foo": "bar"}'
+    assert response['isBase64Encoded']
+
+
 def test_can_parse_route_view_args():
     entry = app.RouteEntry(lambda: {"foo": "bar"}, 'view-name',
                            '/foo/{bar}/baz/{qux}', method='GET')