From f1881b645021493b52f1cdd9420760b1b9baa211 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 10:17:19 +0900 Subject: [PATCH 01/16] :wrench: migrate https://github.com/zappa/Zappa/pull/971 to lastest master --- tests/test_binary_support_settings.py | 12 +++ tests/test_handler.py | 128 +++++++++++++++++++++++++- tests/test_wsgi_binary_support_app.py | 63 +++++++++++++ tests/utils.py | 14 ++- zappa/handler.py | 26 +++++- 5 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 tests/test_binary_support_settings.py create mode 100644 tests/test_wsgi_binary_support_app.py diff --git a/tests/test_binary_support_settings.py b/tests/test_binary_support_settings.py new file mode 100644 index 000000000..484fc74dd --- /dev/null +++ b/tests/test_binary_support_settings.py @@ -0,0 +1,12 @@ +API_STAGE = "dev" +APP_FUNCTION = "app" +APP_MODULE = "tests.test_wsgi_binary_support_app" +BINARY_SUPPORT = True +CONTEXT_HEADER_MAPPINGS = {} +DEBUG = "True" +DJANGO_SETTINGS = None +DOMAIN = "api.example.com" +ENVIRONMENT_VARIABLES = {} +LOG_LEVEL = "DEBUG" +PROJECT_NAME = "binary_support_settings" +COGNITO_TRIGGER_MAPPING = {} \ No newline at end of file diff --git a/tests/test_handler.py b/tests/test_handler.py index cc0590128..6beea38fd 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,11 +1,11 @@ -import sys import unittest - from mock import Mock from zappa.handler import LambdaHandler from zappa.utilities import merge_headers +from .utils import is_base64 + def no_args(): return @@ -223,6 +223,130 @@ def test_exception_handler_on_web_request(self): self.assertEqual(response["statusCode"], 500) mocked_exception_handler.assert_called() + def test_wsgi_script_binary_support_with_content_encoding(self): + """ + Ensure that response body is base64 encoded when BINARY_SUPPORT is enabled and Content-Encoding header is present. + """ # don't linebreak so that whole line is shown during nosetest readout + lh = LambdaHandler("tests.test_binary_support_settings") + + text_plain_event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/content_encoding_header_json1", + } + + # A likely scenario is that the application would be gzip compressing some json response. That's checked first. + response = lh.handler(text_plain_event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + + # We also verify that some unknown mimetype with a Content-Encoding also encodes to b64. This route serves + # bytes in the response. + + text_arbitrary_event = { + **text_plain_event, + **{"path": "/content_encoding_header_textarbitrary1"}, + } + + response = lh.handler(text_arbitrary_event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + + # This route is similar to the above, but it serves its response as text and not bytes. That the response + # isn't bytes shouldn't matter because it still has a Content-Encoding header. + + application_json_event = { + **text_plain_event, + **{"path": "/content_encoding_header_textarbitrary2"}, + } + + response = lh.handler(application_json_event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + + def test_wsgi_script_binary_support_without_content_encoding_edgecases( + self, + ): + """ + Ensure zappa response bodies are NOT base64 encoded when BINARY_SUPPORT is enabled and the mimetype is "application/json" or starts with "text/". + """ # don't linebreak so that whole line is shown during nosetest readout + + lh = LambdaHandler("tests.test_binary_support_settings") + + text_plain_event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/textplain_mimetype_response1", + } + + for path in [ + "/textplain_mimetype_response1", # text/plain mimetype should not be turned to base64 + "/textarbitrary_mimetype_response1", # text/arbitrary mimetype should not be turned to base64 + "/json_mimetype_response1", # application/json mimetype should not be turned to base64 + ]: + event = {**text_plain_event, "path": path} + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertNotIn("isBase64Encoded", response) + self.assertFalse(is_base64(response["body"])) + + def test_wsgi_script_binary_support_without_content_encoding( + self, + ): + """ + Ensure zappa response bodies are base64 encoded when BINARY_SUPPORT is enabled and Content-Encoding is absent. + """ # don't linebreak so that whole line is shown during nosetest readout + + lh = LambdaHandler("tests.test_binary_support_settings") + + text_plain_event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/textplain_mimetype_response1", + } + + for path in [ + "/arbitrarybinary_mimetype_response1", + "/arbitrarybinary_mimetype_response2", + ]: + event = {**text_plain_event, "path": path} + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + def test_wsgi_script_on_cognito_event_request(self): """ Ensure that requests sent by cognito behave sensibly diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py new file mode 100644 index 000000000..0fbdd9a5f --- /dev/null +++ b/tests/test_wsgi_binary_support_app.py @@ -0,0 +1,63 @@ +### +# This test application exists to confirm how Zappa handles WSGI application +# _responses_ when Binary Support is enabled. +### + +import gzip +import json + +from flask import Flask, Response, send_file + +app = Flask(__name__) + + +@app.route("/textplain_mimetype_response1", methods=["GET"]) +def text_mimetype_response_1(): + return Response(response="OK", mimetype="text/plain") + + +@app.route("/textarbitrary_mimetype_response1", methods=["GET"]) +def text_mimetype_response_2(): + return Response(response="OK", mimetype="text/arbitary") + + +@app.route("/json_mimetype_response1", methods=["GET"]) +def json_mimetype_response_1(): + return Response(response=json.dumps({"some": "data"}), mimetype="application/json") + + +@app.route("/arbitrarybinary_mimetype_response1", methods=["GET"]) +def arbitrary_mimetype_response_1(): + return Response(response=b"some binary data", mimetype="arbitrary/binary_mimetype") + + +@app.route("/arbitrarybinary_mimetype_response2", methods=["GET"]) +def arbitrary_mimetype_response_3(): + return Response(response="doesnt_matter", mimetype="definitely_not_text") + + +@app.route("/content_encoding_header_json1", methods=["GET"]) +def response_with_content_encoding_1(): + return Response( + response=gzip.compress(json.dumps({"some": "data"}).encode()), + mimetype="application/json", + headers={"Content-Encoding": "gzip"}, + ) + + +@app.route("/content_encoding_header_textarbitrary1", methods=["GET"]) +def response_with_content_encoding_2(): + return Response( + response=b"OK", + mimetype="text/arbitrary", + headers={"Content-Encoding": "something_arbitrarily_binary"}, + ) + + +@app.route("/content_encoding_header_textarbitrary2", methods=["GET"]) +def response_with_content_encoding_3(): + return Response( + response="OK", + mimetype="text/arbitrary", + headers={"Content-Encoding": "with_content_type_but_not_bytes_response"}, + ) \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py index 91e588cc4..8a2a93fad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +import base64 import functools import os from contextlib import contextmanager @@ -6,10 +7,7 @@ import placebo from mock import MagicMock, patch -try: - file -except NameError: # builtin 'file' was removed in Python 3 - from io import IOBase as file +from io import IOBase as file PLACEBO_DIR = os.path.join(os.path.dirname(__file__), "placebo") @@ -72,3 +70,11 @@ def stub_open(*args, **kwargs): with patch("__builtin__.open", stub_open): yield mock_open, mock_file + + +def is_base64(test_string: str) -> bool: + # Taken from https://stackoverflow.com/a/45928164/3200002 + try: + return base64.b64encode(base64.b64decode(test_string)).decode() == test_string + except Exception: + return False \ No newline at end of file diff --git a/zappa/handler.py b/zappa/handler.py index ed0cc9835..f44274c3b 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -557,12 +557,28 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - if ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" + # We base64 encode for two reasons when BINARY_SUPPORT is enabled: + # - Content-Encoding is present, which is commonly used by compression mechanisms to indicate + # that the content is in br/gzip/deflate/etc encoding + # (Related: https://github.com/zappa/Zappa/issues/908). Content like this must be + # transmitted as b64. + # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it + # isn't application/json or text/) + if settings.BINARY_SUPPORT and response.headers.get( + "Content-Encoding" ): - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf-8") + zappa_returndict["body"] = base64.b64encode( + response.data + ).decode() + zappa_returndict["isBase64Encoded"] = True + elif ( + settings.BINARY_SUPPORT + and not response.mimetype.startswith("text/") + and response.mimetype != "application/json" + ): + zappa_returndict["body"] = base64.b64encode( + response.data + ).decode("utf8") zappa_returndict["isBase64Encoded"] = True else: zappa_returndict["body"] = response.get_data(as_text=True) From 19f74a9b7ce3af6aff6897af46ae299437b6bdd7 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 10:22:19 +0900 Subject: [PATCH 02/16] :art: run black/isort --- tests/test_binary_support_settings.py | 2 +- tests/test_handler.py | 1 + tests/test_wsgi_binary_support_app.py | 2 +- tests/utils.py | 5 ++--- zappa/handler.py | 18 ++++++------------ 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/test_binary_support_settings.py b/tests/test_binary_support_settings.py index 484fc74dd..adb257d55 100644 --- a/tests/test_binary_support_settings.py +++ b/tests/test_binary_support_settings.py @@ -9,4 +9,4 @@ ENVIRONMENT_VARIABLES = {} LOG_LEVEL = "DEBUG" PROJECT_NAME = "binary_support_settings" -COGNITO_TRIGGER_MAPPING = {} \ No newline at end of file +COGNITO_TRIGGER_MAPPING = {} diff --git a/tests/test_handler.py b/tests/test_handler.py index 6beea38fd..5cf02c5b9 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,4 +1,5 @@ import unittest + from mock import Mock from zappa.handler import LambdaHandler diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py index 0fbdd9a5f..d1d2e6638 100644 --- a/tests/test_wsgi_binary_support_app.py +++ b/tests/test_wsgi_binary_support_app.py @@ -60,4 +60,4 @@ def response_with_content_encoding_3(): response="OK", mimetype="text/arbitrary", headers={"Content-Encoding": "with_content_type_but_not_bytes_response"}, - ) \ No newline at end of file + ) diff --git a/tests/utils.py b/tests/utils.py index 8a2a93fad..c779adf67 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,13 +2,12 @@ import functools import os from contextlib import contextmanager +from io import IOBase as file import boto3 import placebo from mock import MagicMock, patch -from io import IOBase as file - PLACEBO_DIR = os.path.join(os.path.dirname(__file__), "placebo") @@ -77,4 +76,4 @@ def is_base64(test_string: str) -> bool: try: return base64.b64encode(base64.b64decode(test_string)).decode() == test_string except Exception: - return False \ No newline at end of file + return False diff --git a/zappa/handler.py b/zappa/handler.py index f44274c3b..c19fee9d4 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -564,21 +564,15 @@ def handler(self, event, context): # transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it # isn't application/json or text/) - if settings.BINARY_SUPPORT and response.headers.get( - "Content-Encoding" - ): - zappa_returndict["body"] = base64.b64encode( - response.data - ).decode() + if settings.BINARY_SUPPORT and response.headers.get("Content-Encoding"): + zappa_returndict["body"] = base64.b64encode(response.data).decode() zappa_returndict["isBase64Encoded"] = True elif ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" + settings.BINARY_SUPPORT + and not response.mimetype.startswith("text/") + and response.mimetype != "application/json" ): - zappa_returndict["body"] = base64.b64encode( - response.data - ).decode("utf8") + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") zappa_returndict["isBase64Encoded"] = True else: zappa_returndict["body"] = response.get_data(as_text=True) From d7fcee4cd1994a522e50bc157d21325ae28df645 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 13:48:33 +0900 Subject: [PATCH 03/16] :recycle: refactor to allow for other binary ignore types based on mimetype. (currently openapi schema can't be passed as text. --- zappa/handler.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index c19fee9d4..1e8359387 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -560,22 +560,19 @@ def handler(self, event, context): # We base64 encode for two reasons when BINARY_SUPPORT is enabled: # - Content-Encoding is present, which is commonly used by compression mechanisms to indicate # that the content is in br/gzip/deflate/etc encoding - # (Related: https://github.com/zappa/Zappa/issues/908). Content like this must be - # transmitted as b64. - # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it - # isn't application/json or text/) - if settings.BINARY_SUPPORT and response.headers.get("Content-Encoding"): - zappa_returndict["body"] = base64.b64encode(response.data).decode() - zappa_returndict["isBase64Encoded"] = True - elif ( - settings.BINARY_SUPPORT - and not response.mimetype.startswith("text/") - and response.mimetype != "application/json" - ): - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True - else: - zappa_returndict["body"] = response.get_data(as_text=True) + # (Related: https://github.com/zappa/Zappa/issues/908). + # Content like this must be transmitted as b64. + # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) + zappa_returndict["body"] = response.get_data(as_text=True) + if settings.BINARY_SUPPORT: + # overwrite zappa_returndict["body"] if necessary + exclude_startswith_mimetypes = ("text/", "application/json", "application/vnd.oai.openapi") # TODO: consider for settings + if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") + zappa_returndict["isBase64Encoded"] = True + elif not response.mimetype.startswith(exclude_startswith_mimetypes): + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") + zappa_returndict["isBase64Encoded"] = True zappa_returndict["statusCode"] = response.status_code if "headers" in event: From 039cbfedf10e330ae7bca09827ff2efeb764d566 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 13:53:16 +0900 Subject: [PATCH 04/16] :art: run black/fix flake8 --- zappa/handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zappa/handler.py b/zappa/handler.py index 1e8359387..0345e12fc 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -566,7 +566,11 @@ def handler(self, event, context): zappa_returndict["body"] = response.get_data(as_text=True) if settings.BINARY_SUPPORT: # overwrite zappa_returndict["body"] if necessary - exclude_startswith_mimetypes = ("text/", "application/json", "application/vnd.oai.openapi") # TODO: consider for settings + exclude_startswith_mimetypes = ( + "text/", + "application/json", + "application/vnd.oai.openapi", + ) # TODO: consider for settings if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") zappa_returndict["isBase64Encoded"] = True From bcdfbe4020674cf8c97b4ce01058253c2802c752 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 13:59:37 +0900 Subject: [PATCH 05/16] :wrench: add EXCEPTION_HANDLER setting --- tests/test_binary_support_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_binary_support_settings.py b/tests/test_binary_support_settings.py index adb257d55..84713e889 100644 --- a/tests/test_binary_support_settings.py +++ b/tests/test_binary_support_settings.py @@ -10,3 +10,4 @@ LOG_LEVEL = "DEBUG" PROJECT_NAME = "binary_support_settings" COGNITO_TRIGGER_MAPPING = {} +EXCEPTION_HANDLER = None From 3f0d1357c9c32672251af4349caa8f841b26f193 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 14:05:50 +0900 Subject: [PATCH 06/16] :bug: fix zappa_returndict["body"] assignment --- zappa/handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 0345e12fc..55db6cf70 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -563,9 +563,7 @@ def handler(self, event, context): # (Related: https://github.com/zappa/Zappa/issues/908). # Content like this must be transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) - zappa_returndict["body"] = response.get_data(as_text=True) if settings.BINARY_SUPPORT: - # overwrite zappa_returndict["body"] if necessary exclude_startswith_mimetypes = ( "text/", "application/json", @@ -578,6 +576,10 @@ def handler(self, event, context): zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") zappa_returndict["isBase64Encoded"] = True + if "body" not in zappa_returndict: + # treat body as text + zappa_returndict["body"] = response.get_data(as_text=True) + zappa_returndict["statusCode"] = response.status_code if "headers" in event: zappa_returndict["headers"] = {} From 583cc4d1c333c0b7a77af165df32702e89da7959 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 14:25:13 +0900 Subject: [PATCH 07/16] :pencil: add temp debug info --- zappa/handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zappa/handler.py b/zappa/handler.py index 55db6cf70..1793adc38 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -563,6 +563,9 @@ def handler(self, event, context): # (Related: https://github.com/zappa/Zappa/issues/908). # Content like this must be transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) + print(f"settings.BINARY_SUPPORT={settings.BINARY_SUPPORT}") + print(f"response.headers.get('Content-Encoding')={response.headers.get('Content-Encoding')}") + print(f"response.mimetype={response.mimetype}") if settings.BINARY_SUPPORT: exclude_startswith_mimetypes = ( "text/", From 20bd12fdb7e9ee74f337741966cccf2ea2701226 Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 15:20:39 +0900 Subject: [PATCH 08/16] :fire: delete unnecessary print statements --- zappa/handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 1793adc38..55db6cf70 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -563,9 +563,6 @@ def handler(self, event, context): # (Related: https://github.com/zappa/Zappa/issues/908). # Content like this must be transmitted as b64. # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) - print(f"settings.BINARY_SUPPORT={settings.BINARY_SUPPORT}") - print(f"response.headers.get('Content-Encoding')={response.headers.get('Content-Encoding')}") - print(f"response.mimetype={response.mimetype}") if settings.BINARY_SUPPORT: exclude_startswith_mimetypes = ( "text/", From 2a6aacd84092c4077d22deb48653904f6ab2150d Mon Sep 17 00:00:00 2001 From: monkut Date: Thu, 28 Jul 2022 16:40:30 +0900 Subject: [PATCH 09/16] :recycle: Update comments and minor refactor for clarity --- zappa/handler.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 55db6cf70..9591a48c6 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -557,27 +557,36 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - # We base64 encode for two reasons when BINARY_SUPPORT is enabled: - # - Content-Encoding is present, which is commonly used by compression mechanisms to indicate - # that the content is in br/gzip/deflate/etc encoding - # (Related: https://github.com/zappa/Zappa/issues/908). - # Content like this must be transmitted as b64. - # - The response is assumed to be some binary format (since BINARY_SUPPORT is enabled and it isn't application/json or text/) + encode_as_base64 = False if settings.BINARY_SUPPORT: + # Related: https://github.com/zappa/Zappa/issues/908 + # API Gateway requires binary data be base64 encoded: + # https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/ + # When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases: + # - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc) + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + # Content like this must be transmitted as b64. + # - Response assumed binary when Response.mimetype does + # not start with entry defined in 'exclude_startswith_mimetypes' exclude_startswith_mimetypes = ( "text/", "application/json", "application/vnd.oai.openapi", ) # TODO: consider for settings if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True + encode_as_base64 = True + + # werkzeug Response.mimetype: lowercase without parameters + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype elif not response.mimetype.startswith(exclude_startswith_mimetypes): - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True + encode_as_base64 = True - if "body" not in zappa_returndict: - # treat body as text + if encode_as_base64: + zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") + zappa_returndict["isBase64Encoded"] = True + else: + # response.data decoded by werkzeug + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data zappa_returndict["body"] = response.get_data(as_text=True) zappa_returndict["statusCode"] = response.status_code From 153366d69c1ce6087c2835d3e819b3ae6dfa667c Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 29 Jul 2022 09:59:13 +0900 Subject: [PATCH 10/16] :recycle: refactor for ease of testing and clarity --- zappa/handler.py | 79 +++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 9591a48c6..3d952e00e 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -10,6 +10,7 @@ import tarfile import traceback from builtins import str +from typing import Tuple import boto3 from werkzeug.wrappers import Response @@ -265,6 +266,49 @@ def _process_exception(cls, exception_handler, event, context, exception): print(cex) return exception_processed + @staticmethod + def _process_response_body(response: Response, binary_support: bool = False) -> Tuple[str, bool]: + """ + Perform Response body encoding/decoding + + Related: https://github.com/zappa/Zappa/issues/908 + API Gateway requires binary data be base64 encoded: + https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/ + When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases: + + - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc) + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + Content like this must be transmitted as b64. + + - Response assumed binary when Response.mimetype does + not start with an entry defined in 'handle_as_text_mimetypes' + """ + encode_body_as_base64 = False + if binary_support: + + handle_as_text_mimetypes = ( + "text/", + "application/json", + "application/vnd.oai.openapi", + ) # TODO: consider for settings + # TODO: woff files ok? + if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding + encode_body_as_base64 = True + + # werkzeug Response.mimetype: lowercase without parameters + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype + elif not response.mimetype.startswith(handle_as_text_mimetypes): + encode_body_as_base64 = True + + if encode_body_as_base64: + body = base64.b64encode(response.data).decode("utf8") + else: + # response.data decoded by werkzeug + # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data + body = response.get_data(as_text=True) + + return body, encode_body_as_base64 + @staticmethod def run_function(app_function, event, context): """ @@ -557,37 +601,10 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - encode_as_base64 = False - if settings.BINARY_SUPPORT: - # Related: https://github.com/zappa/Zappa/issues/908 - # API Gateway requires binary data be base64 encoded: - # https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/ - # When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases: - # - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc) - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding - # Content like this must be transmitted as b64. - # - Response assumed binary when Response.mimetype does - # not start with entry defined in 'exclude_startswith_mimetypes' - exclude_startswith_mimetypes = ( - "text/", - "application/json", - "application/vnd.oai.openapi", - ) # TODO: consider for settings - if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding - encode_as_base64 = True - - # werkzeug Response.mimetype: lowercase without parameters - # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype - elif not response.mimetype.startswith(exclude_startswith_mimetypes): - encode_as_base64 = True - - if encode_as_base64: - zappa_returndict["body"] = base64.b64encode(response.data).decode("utf8") - zappa_returndict["isBase64Encoded"] = True - else: - # response.data decoded by werkzeug - # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data - zappa_returndict["body"] = response.get_data(as_text=True) + processed_body, is_base64_encoded = self._process_response_body(response, binary_support=settings.BINARY_SUPPORT) + zappa_returndict["body"] = processed_body + if is_base64_encoded: + zappa_returndict["isBase64Encoded"] = is_base64_encoded zappa_returndict["statusCode"] = response.status_code if "headers" in event: From 4ddfaa53a8c179a4cbca597270eb9d57411cbdb9 Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 29 Jul 2022 10:21:13 +0900 Subject: [PATCH 11/16] :art: fix flake8 --- zappa/handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zappa/handler.py b/zappa/handler.py index 3d952e00e..a075d6046 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -601,7 +601,9 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - processed_body, is_base64_encoded = self._process_response_body(response, binary_support=settings.BINARY_SUPPORT) + processed_body, is_base64_encoded = self._process_response_body( + response, binary_support=settings.BINARY_SUPPORT + ) zappa_returndict["body"] = processed_body if is_base64_encoded: zappa_returndict["isBase64Encoded"] = is_base64_encoded From 0370119a794a3356f5158fb19135cb46a952fad5 Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 5 Aug 2022 18:44:18 +0900 Subject: [PATCH 12/16] :sparkles: add `additional_text_mimetypes` setting :white_check_mark: add testcases for additional_text_mimetypes handling --- README.md | 1 + test_settings.json | 9 ++- ...ad_additional_text_mimetypes_settings.json | 9 +++ ...port_additional_text_mimetypes_settings.py | 14 +++++ tests/test_handler.py | 58 +++++++++++++++++++ tests/test_wsgi_binary_support_app.py | 8 +++ tests/tests.py | 14 +++++ zappa/cli.py | 11 ++++ zappa/handler.py | 21 +++---- 9 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 tests/test_bad_additional_text_mimetypes_settings.json create mode 100644 tests/test_binary_support_additional_text_mimetypes_settings.py diff --git a/README.md b/README.md index 911860667..c56df1f51 100644 --- a/README.md +++ b/README.md @@ -820,6 +820,7 @@ to change Zappa's behavior. Use these at your own risk! ```javascript { "dev": { + "additional_text_mimetypes": [], // allows you to provide additional mimetypes to be handled as text when binary_support is true. "alb_enabled": false, // enable provisioning of application load balancing resources. If set to true, you _must_ fill out the alb_vpc_config option as well. "alb_vpc_config": { "CertificateArn": "your_acm_certificate_arn", // ACM certificate ARN for ALB diff --git a/test_settings.json b/test_settings.json index f7b29bfc2..2ee126a1f 100644 --- a/test_settings.json +++ b/test_settings.json @@ -120,5 +120,12 @@ "lambda_concurrency_enabled": { "extends": "ttt888", "lambda_concurrency": 6 - } + }, + "addtextmimetypes": { + "s3_bucket": "lmbda", + "app_function": "tests.test_app.hello_world", + "delete_local_zip": true, + "binary_support": true, + "additional_text_mimetypes": ["application/custommimetype"] + } } diff --git a/tests/test_bad_additional_text_mimetypes_settings.json b/tests/test_bad_additional_text_mimetypes_settings.json new file mode 100644 index 000000000..b3a09f57f --- /dev/null +++ b/tests/test_bad_additional_text_mimetypes_settings.json @@ -0,0 +1,9 @@ +{ + "nobinarysupport": { + "s3_bucket": "lmbda", + "app_function": "tests.test_app.hello_world", + "delete_local_zip": true, + "binary_support": false, + "additional_text_mimetypes": ["application/custommimetype"] + } +} \ No newline at end of file diff --git a/tests/test_binary_support_additional_text_mimetypes_settings.py b/tests/test_binary_support_additional_text_mimetypes_settings.py new file mode 100644 index 000000000..27c70c1bc --- /dev/null +++ b/tests/test_binary_support_additional_text_mimetypes_settings.py @@ -0,0 +1,14 @@ +API_STAGE = "dev" +APP_FUNCTION = "app" +APP_MODULE = "tests.test_wsgi_binary_support_app" +BINARY_SUPPORT = True +CONTEXT_HEADER_MAPPINGS = {} +DEBUG = "True" +DJANGO_SETTINGS = None +DOMAIN = "api.example.com" +ENVIRONMENT_VARIABLES = {} +LOG_LEVEL = "DEBUG" +PROJECT_NAME = "binary_support_settings" +COGNITO_TRIGGER_MAPPING = {} +EXCEPTION_HANDLER = None +ADDITIONAL_TEXT_MIMETYPES = ["application/vnd.oai.openapi"] diff --git a/tests/test_handler.py b/tests/test_handler.py index 5cf02c5b9..b8cb59fee 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -348,6 +348,64 @@ def test_wsgi_script_binary_support_without_content_encoding( self.assertIn("isBase64Encoded", response) self.assertTrue(is_base64(response["body"])) + def test_wsgi_script_binary_support_userdefined_additional_text_mimetypes__defined( + self, + ): + """ + Ensure zappa response bodies are NOT base64 encoded when BINARY_SUPPORT is True, and additional_text_mimetypes are defined + """ + lh = LambdaHandler("tests.test_binary_support_additional_text_mimetypes_settings") + expected_additional_mimetypes = ["application/vnd.oai.openapi"] + self.assertEqual(lh.settings.ADDITIONAL_TEXT_MIMETYPES, expected_additional_mimetypes) + + event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/userdefined_additional_mimetype_response1", + } + + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertNotIn("isBase64Encoded", response) + self.assertFalse(is_base64(response["body"])) + + def test_wsgi_script_binary_support_userdefined_additional_text_mimetypes__undefined( + self, + ): + """ + Ensure zappa response bodies are base64 encoded when BINARY_SUPPORT is True and mimetype not defined in additional_text_mimetypes + """ + lh = LambdaHandler("tests.test_binary_support_settings") + + event = { + "body": "", + "resource": "/{proxy+}", + "requestContext": {}, + "queryStringParameters": {}, + "headers": { + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + }, + "pathParameters": {"proxy": "return/request/url"}, + "httpMethod": "GET", + "stageVariables": {}, + "path": "/userdefined_additional_mimetype_response1", + } + + response = lh.handler(event, None) + + self.assertEqual(response["statusCode"], 200) + self.assertIn("isBase64Encoded", response) + self.assertTrue(is_base64(response["body"])) + def test_wsgi_script_on_cognito_event_request(self): """ Ensure that requests sent by cognito behave sensibly diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py index d1d2e6638..b4c4ea504 100644 --- a/tests/test_wsgi_binary_support_app.py +++ b/tests/test_wsgi_binary_support_app.py @@ -61,3 +61,11 @@ def response_with_content_encoding_3(): mimetype="text/arbitrary", headers={"Content-Encoding": "with_content_type_but_not_bytes_response"}, ) + + +@app.route("/userdefined_additional_mimetype_response1", methods=["GET"]) +def response_with_userdefined_addtional_mimetype(): + return Response( + response="OK", + mimetype="application/vnd.oai.openapi", + ) diff --git a/tests/tests.py b/tests/tests.py index 831574606..61f572e5e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1074,6 +1074,20 @@ def test_load_settings_toml(self): zappa_cli.load_settings("tests/test_settings.toml") self.assertEqual(False, zappa_cli.stage_config["touch"]) + def test_load_settings_bad_additional_text_mimetypes(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "nobinarysupport" + with self.assertRaises(ClickException): + zappa_cli.load_settings("tests/test_bad_additional_text_mimetypes_settings.json") + + def test_load_settings_additional_text_mimetypes(self): + zappa_cli = ZappaCLI() + zappa_cli.api_stage = "addtextmimetypes" + zappa_cli.load_settings("test_settings.json") + expected_additional_text_mimetypes = ["application/custommimetype"] + self.assertEqual(expected_additional_text_mimetypes, zappa_cli.stage_config["additional_text_mimetypes"]) + self.assertEqual(True, zappa_cli.stage_config["binary_support"]) + def test_settings_extension(self): """ Make sure Zappa uses settings in the proper order: JSON, TOML, YAML. diff --git a/zappa/cli.py b/zappa/cli.py index 305e74e30..0e4ac6272 100755 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -67,6 +67,7 @@ BOTO3_CONFIG_DOCS_URL = "https://boto3.readthedocs.io/en/latest/guide/quickstart.html#configuration" + ## # Main Input Processing ## @@ -117,6 +118,7 @@ class ZappaCLI: xray_tracing = False aws_kms_key_arn = "" context_header_mappings = None + additional_text_mimetypes = None tags = [] layers = None @@ -2290,6 +2292,11 @@ def load_settings(self, settings_file=None, session=None): self.xray_tracing = self.stage_config.get("xray_tracing", False) self.desired_role_arn = self.stage_config.get("role_arn") self.layers = self.stage_config.get("layers", None) + self.additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", None) + + # check that BINARY_SUPPORT is True if additional_text_mimetypes is provided + if self.additional_text_mimetypes and not self.binary_support: + raise ClickException("zappa_settings.json has additional_text_mimetypes defined, but binary_support is False!") # Load ALB-related settings self.use_alb = self.stage_config.get("alb_enabled", False) @@ -2622,6 +2629,10 @@ def get_zappa_settings_string(self): # async response async_response_table = self.stage_config.get("async_response_table", "") settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table) + + # additional_text_mimetypes + additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", []) + settings_s += f"ADDITIONAL_TEXT_MIMETYPES={additional_text_mimetypes}\n" return settings_s def remove_local_zip(self): diff --git a/zappa/handler.py b/zappa/handler.py index a075d6046..88976b4d7 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -10,6 +10,7 @@ import tarfile import traceback from builtins import str +from types import ModuleType from typing import Tuple import boto3 @@ -267,7 +268,7 @@ def _process_exception(cls, exception_handler, event, context, exception): return exception_processed @staticmethod - def _process_response_body(response: Response, binary_support: bool = False) -> Tuple[str, bool]: + def _process_response_body(response: Response, settings: ModuleType) -> Tuple[str, bool]: """ Perform Response body encoding/decoding @@ -284,14 +285,12 @@ def _process_response_body(response: Response, binary_support: bool = False) -> not start with an entry defined in 'handle_as_text_mimetypes' """ encode_body_as_base64 = False - if binary_support: - - handle_as_text_mimetypes = ( - "text/", - "application/json", - "application/vnd.oai.openapi", - ) # TODO: consider for settings - # TODO: woff files ok? + if settings.BINARY_SUPPORT: + handle_as_text_mimetypes = ("text/", "application/json") + additional_text_mimetypes = getattr(settings, "ADDITIONAL_TEXT_MIMETYPES", None) + if additional_text_mimetypes: + handle_as_text_mimetypes += tuple(additional_text_mimetypes) + if response.headers.get("Content-Encoding"): # Assume br/gzip/deflate/etc encoding encode_body_as_base64 = True @@ -601,9 +600,7 @@ def handler(self, event, context): zappa_returndict.setdefault("statusDescription", response.status) if response.data: - processed_body, is_base64_encoded = self._process_response_body( - response, binary_support=settings.BINARY_SUPPORT - ) + processed_body, is_base64_encoded = self._process_response_body(response, settings=settings) zappa_returndict["body"] = processed_body if is_base64_encoded: zappa_returndict["isBase64Encoded"] = is_base64_encoded From 71c8aa36db8f97f325d12f4fb517b0a16308ea1c Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 12 Aug 2022 18:29:57 +0900 Subject: [PATCH 13/16] :wrench: Expand default text mimetypes mentioned in https://github.com/zappa/Zappa/pull/1023 :recycle: define "DEFAULT_TEXT_MIMETYPES" and move to utilities.py --- zappa/handler.py | 6 +++--- zappa/utilities.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 88976b4d7..920ad928f 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -20,11 +20,11 @@ # so handle both scenarios. try: from zappa.middleware import ZappaWSGIMiddleware - from zappa.utilities import merge_headers, parse_s3_url + from zappa.utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES from zappa.wsgi import common_log, create_wsgi_request except ImportError: # pragma: no cover from .middleware import ZappaWSGIMiddleware - from .utilities import merge_headers, parse_s3_url + from .utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES from .wsgi import common_log, create_wsgi_request @@ -286,7 +286,7 @@ def _process_response_body(response: Response, settings: ModuleType) -> Tuple[st """ encode_body_as_base64 = False if settings.BINARY_SUPPORT: - handle_as_text_mimetypes = ("text/", "application/json") + handle_as_text_mimetypes = DEFAULT_TEXT_MIMETYPES additional_text_mimetypes = getattr(settings, "ADDITIONAL_TEXT_MIMETYPES", None) if additional_text_mimetypes: handle_as_text_mimetypes += tuple(additional_text_mimetypes) diff --git a/zappa/utilities.py b/zappa/utilities.py index 72ad9f0f7..87a7fa3b3 100644 --- a/zappa/utilities.py +++ b/zappa/utilities.py @@ -20,6 +20,19 @@ # Settings / Packaging ## +# mimetypes starting with entries defined here are considered as TEXT when BINARTY_SUPPORT is True. +# - Additional TEXT mimetypes may be defined with the 'ADDITIONAL_TEXT_MIMETYPES' setting. +DEFAULT_TEXT_MIMETYPES = ( + "text/", + "application/json", # RFC 4627 + "application/javascript", # RFC 4329 + "application/ecmascript", # RFC 4329 + "application/xml", # RFC 3023 + "application/xml-external-parsed-entity", # RFC 3023 + "application/xml-dtd", # RFC 3023 + "image/svg+xml", # RFC 3023 +) + def copytree(src, dst, metadata=True, symlinks=False, ignore=None): """ From 48057fcb5ef3a5d31c48631dde31ef306181273a Mon Sep 17 00:00:00 2001 From: monkut Date: Fri, 12 Aug 2022 18:30:28 +0900 Subject: [PATCH 14/16] :art: run black/isort --- zappa/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zappa/handler.py b/zappa/handler.py index 920ad928f..1c6fdb0fd 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -20,11 +20,11 @@ # so handle both scenarios. try: from zappa.middleware import ZappaWSGIMiddleware - from zappa.utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES + from zappa.utilities import DEFAULT_TEXT_MIMETYPES, merge_headers, parse_s3_url from zappa.wsgi import common_log, create_wsgi_request except ImportError: # pragma: no cover from .middleware import ZappaWSGIMiddleware - from .utilities import merge_headers, parse_s3_url, DEFAULT_TEXT_MIMETYPES + from .utilities import DEFAULT_TEXT_MIMETYPES, merge_headers, parse_s3_url from .wsgi import common_log, create_wsgi_request From 58e221b629ac7a155ca652c981f1f018b8d2c3ce Mon Sep 17 00:00:00 2001 From: monkut Date: Sat, 22 Oct 2022 11:06:46 +0900 Subject: [PATCH 15/16] :art: run black/isort --- tests/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 0954141f9..725947c84 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -84,4 +84,3 @@ def get_unsupported_sys_versioninfo() -> tuple: """Mock used to test the python unsupported version testcase""" invalid_versioninfo = namedtuple("version_info", ["major", "minor", "micro", "releaselevel", "serial"]) return invalid_versioninfo(3, 6, 1, "final", 0) - From 15532f1e5a1f285c93ad901c01602727b045684e Mon Sep 17 00:00:00 2001 From: monkut Date: Mon, 7 Nov 2022 09:31:52 +0900 Subject: [PATCH 16/16] :art: remove unnecesasry comment (black now reformats code) :art: change commented lines to docstring for test app --- tests/test_handler.py | 6 +++--- tests/test_wsgi_binary_support_app.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_handler.py b/tests/test_handler.py index b8cb59fee..c15159567 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -227,7 +227,7 @@ def test_exception_handler_on_web_request(self): def test_wsgi_script_binary_support_with_content_encoding(self): """ Ensure that response body is base64 encoded when BINARY_SUPPORT is enabled and Content-Encoding header is present. - """ # don't linebreak so that whole line is shown during nosetest readout + """ lh = LambdaHandler("tests.test_binary_support_settings") text_plain_event = { @@ -284,7 +284,7 @@ def test_wsgi_script_binary_support_without_content_encoding_edgecases( ): """ Ensure zappa response bodies are NOT base64 encoded when BINARY_SUPPORT is enabled and the mimetype is "application/json" or starts with "text/". - """ # don't linebreak so that whole line is shown during nosetest readout + """ lh = LambdaHandler("tests.test_binary_support_settings") @@ -319,7 +319,7 @@ def test_wsgi_script_binary_support_without_content_encoding( ): """ Ensure zappa response bodies are base64 encoded when BINARY_SUPPORT is enabled and Content-Encoding is absent. - """ # don't linebreak so that whole line is shown during nosetest readout + """ lh = LambdaHandler("tests.test_binary_support_settings") diff --git a/tests/test_wsgi_binary_support_app.py b/tests/test_wsgi_binary_support_app.py index b4c4ea504..b4d9bfb50 100644 --- a/tests/test_wsgi_binary_support_app.py +++ b/tests/test_wsgi_binary_support_app.py @@ -1,12 +1,12 @@ -### -# This test application exists to confirm how Zappa handles WSGI application -# _responses_ when Binary Support is enabled. -### +""" +This test application exists to confirm how Zappa handles WSGI application +_responses_ when Binary Support is enabled. +""" import gzip import json -from flask import Flask, Response, send_file +from flask import Flask, Response app = Flask(__name__)