From 44abceeb3a93f3b167f547c77b582f8916bf05a2 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Sun, 4 Jul 2021 11:28:35 -0700
Subject: [PATCH 01/10] feat(api-gateway): add debug mode

---
 .../event_handler/api_gateway.py              | 30 +++++++++++++-
 aws_lambda_powertools/shared/constants.py     |  2 +
 .../event_handler/test_api_gateway.py         | 41 +++++++++++++++++++
 3 files changed, 71 insertions(+), 2 deletions(-)

diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 2b1e1fc0900..93f7823606c 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -1,11 +1,15 @@
 import base64
 import json
 import logging
+import os
 import re
+import traceback
 import zlib
 from enum import Enum
 from typing import Any, Callable, Dict, List, Optional, Set, Union
 
+from aws_lambda_powertools.shared import constants
+from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice
 from aws_lambda_powertools.shared.json_encoder import Encoder
 from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2
 from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
@@ -237,7 +241,12 @@ def lambda_handler(event, context):
     current_event: BaseProxyEvent
     lambda_context: LambdaContext
 
-    def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors: CORSConfig = None):
+    def __init__(
+        self,
+        proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent,
+        cors: CORSConfig = None,
+        debug: Optional[bool] = None,
+    ):
         """
         Parameters
         ----------
@@ -245,12 +254,15 @@ def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors:
             Proxy request type, defaults to API Gateway V1
         cors: CORSConfig
             Optionally configure and enabled CORS. Not each route will need to have to cors=True
+        debug: Optional[bool]
+            Enables debug mode, by default False, can be enabled by an environement variable
         """
         self._proxy_type = proxy_type
         self._routes: List[Route] = []
         self._cors = cors
         self._cors_enabled: bool = cors is not None
         self._cors_methods: Set[str] = {"OPTIONS"}
+        self.debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false"))
 
     def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None):
         """Get route decorator with GET `method`
@@ -475,7 +487,21 @@ def _not_found(self, method: str) -> ResponseBuilder:
 
     def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder:
         """Actually call the matching route with any provided keyword arguments."""
-        return ResponseBuilder(self._to_response(route.func(**args)), route)
+        try:
+            return ResponseBuilder(self._to_response(route.func(**args)), route)
+        except Exception:
+            if self.debug:
+                # If the user has turned on debug mode,
+                # we'll let the original exception propagate so
+                # they get more information about what went wrong.
+                return ResponseBuilder(
+                    Response(
+                        status_code=500,
+                        content_type="text/plain",
+                        body="".join(traceback.format_exc()),
+                    )
+                )
+            raise
 
     @staticmethod
     def _to_response(result: Union[Dict, Response]) -> Response:
diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py
index eaad5640dfd..a9008d86db9 100644
--- a/aws_lambda_powertools/shared/constants.py
+++ b/aws_lambda_powertools/shared/constants.py
@@ -18,3 +18,5 @@
 
 XRAY_SDK_MODULE = "aws_xray_sdk"
 XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core"
+
+API_DEBUG_ENV: str = "POWERTOOLS_API_DEBUG"
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index caaaeb1b97b..6205a925eaf 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -5,6 +5,8 @@
 from pathlib import Path
 from typing import Dict
 
+import pytest
+
 from aws_lambda_powertools.event_handler.api_gateway import (
     ApiGatewayResolver,
     CORSConfig,
@@ -490,3 +492,42 @@ def custom_method():
     assert headers["Content-Type"] == TEXT_HTML
     assert "Access-Control-Allow-Origin" in result["headers"]
     assert headers["Access-Control-Allow-Methods"] == "CUSTOM"
+
+
+def test_unhandled_exceptions_debug_on():
+    # GIVEN debug is enabled
+    # AND an unhandlable exception is raised
+    app = ApiGatewayResolver(debug=True)
+
+    @app.get("/raises-error")
+    def raises_error():
+        raise RuntimeError("Foo")
+
+    # WHEN calling the handler
+    result = app({"path": "/raises-error", "httpMethod": "GET"}, None)
+
+    # THEN return a 500
+    # AND Content-Type is set to text/plain
+    # AND include the exception traceback in the response
+    assert result["statusCode"] == 500
+    assert "Traceback (most recent call last)" in result["body"]
+    headers = result["headers"]
+    assert headers["Content-Type"] == "text/plain"
+
+
+def test_unhandled_exceptions_debug_off():
+    # GIVEN debug is disabled
+    # AND an unhandlable exception is raised
+    app = ApiGatewayResolver(debug=False)
+
+    @app.get("/raises-error")
+    def raises_error():
+        raise RuntimeError("Foo")
+
+    # WHEN calling the handler
+    # THEN raise the original exception
+    with pytest.raises(RuntimeError) as e:
+        app({"path": "/raises-error", "httpMethod": "GET"}, None)
+
+    # AND include the original error
+    assert e.value.args == ("Foo",)

From 7d73c042289cbd3184caddbc9c22924f1cc21468 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Sun, 4 Jul 2021 17:20:40 -0700
Subject: [PATCH 02/10] test(api-gateway): test for debug environment variable

---
 tests/functional/event_handler/test_api_gateway.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index 6205a925eaf..aeabdf72a3f 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -14,6 +14,7 @@
     Response,
     ResponseBuilder,
 )
+from aws_lambda_powertools.shared import constants
 from aws_lambda_powertools.shared.json_encoder import Encoder
 from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2
 from tests.functional.utils import load_event
@@ -498,6 +499,7 @@ def test_unhandled_exceptions_debug_on():
     # GIVEN debug is enabled
     # AND an unhandlable exception is raised
     app = ApiGatewayResolver(debug=True)
+    assert app.debug
 
     @app.get("/raises-error")
     def raises_error():
@@ -519,6 +521,7 @@ def test_unhandled_exceptions_debug_off():
     # GIVEN debug is disabled
     # AND an unhandlable exception is raised
     app = ApiGatewayResolver(debug=False)
+    assert not app.debug
 
     @app.get("/raises-error")
     def raises_error():
@@ -531,3 +534,13 @@ def raises_error():
 
     # AND include the original error
     assert e.value.args == ("Foo",)
+
+
+def test_debug_mode_environment_variable(monkeypatch):
+    # GIVEN a debug mode environment variable is set
+    monkeypatch.setenv(constants.API_DEBUG_ENV, "true")
+    app = ApiGatewayResolver()
+
+    # WHEN calling app.debug
+    # THEN the debug mode is enabled
+    assert app.debug

From 3f994e6a2a3a62a446af83c9f7850ccac7275762 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Sun, 4 Jul 2021 17:28:26 -0700
Subject: [PATCH 03/10] refactor(api-gateway): rename to _debug and fix
 spelling

---
 aws_lambda_powertools/event_handler/api_gateway.py |  6 +++---
 tests/functional/event_handler/test_api_gateway.py | 12 ++++++------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 93f7823606c..456a852770e 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -255,14 +255,14 @@ def __init__(
         cors: CORSConfig
             Optionally configure and enabled CORS. Not each route will need to have to cors=True
         debug: Optional[bool]
-            Enables debug mode, by default False, can be enabled by an environement variable
+            Enables debug mode, by default False. Can be enabled by "POWERTOOLS_API_DEBUG" environment variable
         """
         self._proxy_type = proxy_type
         self._routes: List[Route] = []
         self._cors = cors
         self._cors_enabled: bool = cors is not None
         self._cors_methods: Set[str] = {"OPTIONS"}
-        self.debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false"))
+        self._debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false"))
 
     def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None):
         """Get route decorator with GET `method`
@@ -490,7 +490,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder:
         try:
             return ResponseBuilder(self._to_response(route.func(**args)), route)
         except Exception:
-            if self.debug:
+            if self._debug:
                 # If the user has turned on debug mode,
                 # we'll let the original exception propagate so
                 # they get more information about what went wrong.
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index aeabdf72a3f..925751bfc36 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -497,9 +497,9 @@ def custom_method():
 
 def test_unhandled_exceptions_debug_on():
     # GIVEN debug is enabled
-    # AND an unhandlable exception is raised
+    # AND an unhandled exception is raised
     app = ApiGatewayResolver(debug=True)
-    assert app.debug
+    assert app._debug
 
     @app.get("/raises-error")
     def raises_error():
@@ -519,9 +519,9 @@ def raises_error():
 
 def test_unhandled_exceptions_debug_off():
     # GIVEN debug is disabled
-    # AND an unhandlable exception is raised
+    # AND an unhandled exception is raised
     app = ApiGatewayResolver(debug=False)
-    assert not app.debug
+    assert not app._debug
 
     @app.get("/raises-error")
     def raises_error():
@@ -541,6 +541,6 @@ def test_debug_mode_environment_variable(monkeypatch):
     monkeypatch.setenv(constants.API_DEBUG_ENV, "true")
     app = ApiGatewayResolver()
 
-    # WHEN calling app.debug
+    # WHEN calling app._debug
     # THEN the debug mode is enabled
-    assert app.debug
+    assert app._debug

From a6760f7bbebf8faf31b6328f44ed113433a3ac94 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Sun, 4 Jul 2021 21:54:55 -0700
Subject: [PATCH 04/10] feat(api-gateway): pretty print json in debug mode

---
 .../event_handler/api_gateway.py                 | 14 ++++++++++----
 .../functional/event_handler/test_api_gateway.py | 16 ++++++++++++++++
 2 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 456a852770e..6ce7a201a94 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -481,7 +481,7 @@ def _not_found(self, method: str) -> ResponseBuilder:
                 status_code=404,
                 content_type="application/json",
                 headers=headers,
-                body=json.dumps({"message": "Not found"}),
+                body=self._json_dump({"message": "Not found"}),
             )
         )
 
@@ -503,8 +503,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder:
                 )
             raise
 
-    @staticmethod
-    def _to_response(result: Union[Dict, Response]) -> Response:
+    def _to_response(self, result: Union[Dict, Response]) -> Response:
         """Convert the route's result to a Response
 
          2 main result types are supported:
@@ -520,5 +519,12 @@ def _to_response(result: Union[Dict, Response]) -> Response:
         return Response(
             status_code=200,
             content_type="application/json",
-            body=json.dumps(result, separators=(",", ":"), cls=Encoder),
+            body=self._json_dump(result),
         )
+
+    def _json_dump(self, obj: Any) -> str:
+        """Does a concise json serialization"""
+        if self._debug:
+            return json.dumps(obj, indent=4, cls=Encoder)
+        else:
+            return json.dumps(obj, separators=(",", ":"), cls=Encoder)
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index 925751bfc36..9278ecc8196 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -544,3 +544,19 @@ def test_debug_mode_environment_variable(monkeypatch):
     # WHEN calling app._debug
     # THEN the debug mode is enabled
     assert app._debug
+
+
+def test_debug_json_formatting():
+    # GIVEN debug is True
+    app = ApiGatewayResolver(debug=True)
+    response = {"message": "Foo"}
+
+    @app.get("/foo")
+    def foo():
+        return response
+
+    # WHEN calling the handler
+    result = app({"path": "/foo", "httpMethod": "GET"}, None)
+
+    # THEN return a pretty print json in the body
+    assert result["body"] == json.dumps(response, indent=4)

From 1e65399bd776fc41a283359ddabe0fcfc8b18ec7 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Mon, 5 Jul 2021 12:07:43 -0700
Subject: [PATCH 05/10] refactor(data-classes): rename to
 POWERTOOLS_EVENT_HANDLER_DEBUG

---
 aws_lambda_powertools/event_handler/api_gateway.py | 7 +++++--
 aws_lambda_powertools/shared/constants.py          | 2 +-
 tests/functional/event_handler/test_api_gateway.py | 2 +-
 3 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 6ce7a201a94..2f779d47a52 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -255,14 +255,17 @@ def __init__(
         cors: CORSConfig
             Optionally configure and enabled CORS. Not each route will need to have to cors=True
         debug: Optional[bool]
-            Enables debug mode, by default False. Can be enabled by "POWERTOOLS_API_DEBUG" environment variable
+            Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG"
+            environment variable
         """
         self._proxy_type = proxy_type
         self._routes: List[Route] = []
         self._cors = cors
         self._cors_enabled: bool = cors is not None
         self._cors_methods: Set[str] = {"OPTIONS"}
-        self._debug = resolve_truthy_env_var_choice(choice=debug, env=os.getenv(constants.API_DEBUG_ENV, "false"))
+        self._debug = resolve_truthy_env_var_choice(
+            choice=debug, env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false")
+        )
 
     def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None):
         """Get route decorator with GET `method`
diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py
index a9008d86db9..05eac6cd8c4 100644
--- a/aws_lambda_powertools/shared/constants.py
+++ b/aws_lambda_powertools/shared/constants.py
@@ -19,4 +19,4 @@
 XRAY_SDK_MODULE = "aws_xray_sdk"
 XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core"
 
-API_DEBUG_ENV: str = "POWERTOOLS_API_DEBUG"
+EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG"
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index 9278ecc8196..e9d6933cf2d 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -538,7 +538,7 @@ def raises_error():
 
 def test_debug_mode_environment_variable(monkeypatch):
     # GIVEN a debug mode environment variable is set
-    monkeypatch.setenv(constants.API_DEBUG_ENV, "true")
+    monkeypatch.setenv(constants.EVENT_HANDLER_DEBUG_ENV, "true")
     app = ApiGatewayResolver()
 
     # WHEN calling app._debug

From fe76aa404f573361b170c0023a9aa02c8df8770e Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Tue, 6 Jul 2021 11:14:59 -0700
Subject: [PATCH 06/10] chore: add text/html content types

---
 .../event_handler/api_gateway.py              | 55 ++++++++++---------
 .../event_handler/content_types.py            |  3 +-
 .../event_handler/test_api_gateway.py         | 39 +++++++------
 3 files changed, 50 insertions(+), 47 deletions(-)

diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 534ff94a1a1..35457b1e05a 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -32,43 +32,46 @@ class ProxyEventType(Enum):
 class CORSConfig(object):
     """CORS Config
 
-
     Examples
     --------
 
     Simple cors example using the default permissive cors, not this should only be used during early prototyping
 
-        from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+    ```python
+    from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
 
-        app = ApiGatewayResolver()
+    app = ApiGatewayResolver()
 
-        @app.get("/my/path", cors=True)
-        def with_cors():
-            return {"message": "Foo"}
+    @app.get("/my/path", cors=True)
+    def with_cors():
+        return {"message": "Foo"}
+    ```
 
     Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors`
     do not include any cors headers.
 
-        from aws_lambda_powertools.event_handler.api_gateway import (
-            ApiGatewayResolver, CORSConfig
-        )
-
-        cors_config = CORSConfig(
-            allow_origin="https://wwww.example.com/",
-            expose_headers=["x-exposed-response-header"],
-            allow_headers=["x-custom-request-header"],
-            max_age=100,
-            allow_credentials=True,
-        )
-        app = ApiGatewayResolver(cors=cors_config)
-
-        @app.get("/my/path")
-        def with_cors():
-            return {"message": "Foo"}
+    ```python
+    from aws_lambda_powertools.event_handler.api_gateway import (
+        ApiGatewayResolver, CORSConfig
+    )
+
+    cors_config = CORSConfig(
+        allow_origin="https://wwww.example.com/",
+        expose_headers=["x-exposed-response-header"],
+        allow_headers=["x-custom-request-header"],
+        max_age=100,
+        allow_credentials=True,
+    )
+    app = ApiGatewayResolver(cors=cors_config)
+
+    @app.get("/my/path")
+    def with_cors():
+        return {"message": "Foo"}
 
-        @app.get("/another-one", cors=False)
-        def without_cors():
-            return {"message": "Foo"}
+    @app.get("/another-one", cors=False)
+    def without_cors():
+        return {"message": "Foo"}
+    ```
     """
 
     _REQUIRED_HEADERS = ["Authorization", "Content-Type", "X-Amz-Date", "X-Api-Key", "X-Amz-Security-Token"]
@@ -512,7 +515,7 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder:
                 return ResponseBuilder(
                     Response(
                         status_code=500,
-                        content_type="text/plain",
+                        content_type=content_types.TEXT_PLAIN,
                         body="".join(traceback.format_exc()),
                     )
                 )
diff --git a/aws_lambda_powertools/event_handler/content_types.py b/aws_lambda_powertools/event_handler/content_types.py
index 00ec3db168e..0f55b1088ad 100644
--- a/aws_lambda_powertools/event_handler/content_types.py
+++ b/aws_lambda_powertools/event_handler/content_types.py
@@ -1,4 +1,5 @@
 # use mimetypes library to be certain, e.g., mimetypes.types_map[".json"]
 
 APPLICATION_JSON = "application/json"
-PLAIN_TEXT = "text/plain"
+TEXT_PLAIN = "text/plain"
+TEXT_HTML = "text/html"
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index ae0a69c66eb..738ebd4794f 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -34,7 +34,6 @@ def read_media(file_name: str) -> bytes:
 
 
 LOAD_GW_EVENT = load_event("apiGatewayProxyEvent.json")
-TEXT_HTML = "text/html"
 
 
 def test_alb_event():
@@ -45,7 +44,7 @@ def test_alb_event():
     def foo():
         assert isinstance(app.current_event, ALBEvent)
         assert app.lambda_context == {}
-        return Response(200, TEXT_HTML, "foo")
+        return Response(200, content_types.TEXT_HTML, "foo")
 
     # WHEN calling the event handler
     result = app(load_event("albEvent.json"), {})
@@ -53,7 +52,7 @@ def foo():
     # THEN process event correctly
     # AND set the current_event type as ALBEvent
     assert result["statusCode"] == 200
-    assert result["headers"]["Content-Type"] == TEXT_HTML
+    assert result["headers"]["Content-Type"] == content_types.TEXT_HTML
     assert result["body"] == "foo"
 
 
@@ -83,7 +82,7 @@ def test_api_gateway():
     @app.get("/my/path")
     def get_lambda() -> Response:
         assert isinstance(app.current_event, APIGatewayProxyEvent)
-        return Response(200, TEXT_HTML, "foo")
+        return Response(200, content_types.TEXT_HTML, "foo")
 
     # WHEN calling the event handler
     result = app(LOAD_GW_EVENT, {})
@@ -91,7 +90,7 @@ def get_lambda() -> Response:
     # THEN process event correctly
     # AND set the current_event type as APIGatewayProxyEvent
     assert result["statusCode"] == 200
-    assert result["headers"]["Content-Type"] == TEXT_HTML
+    assert result["headers"]["Content-Type"] == content_types.TEXT_HTML
     assert result["body"] == "foo"
 
 
@@ -103,7 +102,7 @@ def test_api_gateway_v2():
     def my_path() -> Response:
         assert isinstance(app.current_event, APIGatewayProxyEventV2)
         post_data = app.current_event.json_body
-        return Response(200, content_types.PLAIN_TEXT, post_data["username"])
+        return Response(200, content_types.TEXT_PLAIN, post_data["username"])
 
     # WHEN calling the event handler
     result = app(load_event("apiGatewayProxyV2Event.json"), {})
@@ -111,7 +110,7 @@ def my_path() -> Response:
     # THEN process event correctly
     # AND set the current_event type as APIGatewayProxyEventV2
     assert result["statusCode"] == 200
-    assert result["headers"]["Content-Type"] == content_types.PLAIN_TEXT
+    assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN
     assert result["body"] == "tom"
 
 
@@ -122,14 +121,14 @@ def test_include_rule_matching():
     @app.get("/<name>/<my_id>")
     def get_lambda(my_id: str, name: str) -> Response:
         assert name == "my"
-        return Response(200, TEXT_HTML, my_id)
+        return Response(200, content_types.TEXT_HTML, my_id)
 
     # WHEN calling the event handler
     result = app(LOAD_GW_EVENT, {})
 
     # THEN
     assert result["statusCode"] == 200
-    assert result["headers"]["Content-Type"] == TEXT_HTML
+    assert result["headers"]["Content-Type"] == content_types.TEXT_HTML
     assert result["body"] == "path"
 
 
@@ -190,11 +189,11 @@ def test_cors():
 
     @app.get("/my/path", cors=True)
     def with_cors() -> Response:
-        return Response(200, TEXT_HTML, "test")
+        return Response(200, content_types.TEXT_HTML, "test")
 
     @app.get("/without-cors")
     def without_cors() -> Response:
-        return Response(200, TEXT_HTML, "test")
+        return Response(200, content_types.TEXT_HTML, "test")
 
     def handler(event, context):
         return app.resolve(event, context)
@@ -205,7 +204,7 @@ def handler(event, context):
     # THEN the headers should include cors headers
     assert "headers" in result
     headers = result["headers"]
-    assert headers["Content-Type"] == TEXT_HTML
+    assert headers["Content-Type"] == content_types.TEXT_HTML
     assert headers["Access-Control-Allow-Origin"] == "*"
     assert "Access-Control-Allow-Credentials" not in headers
     assert headers["Access-Control-Allow-Headers"] == ",".join(sorted(CORSConfig._REQUIRED_HEADERS))
@@ -271,7 +270,7 @@ def test_compress_no_accept_encoding():
 
     @app.get("/my/path", compress=True)
     def return_text() -> Response:
-        return Response(200, content_types.PLAIN_TEXT, expected_value)
+        return Response(200, content_types.TEXT_PLAIN, expected_value)
 
     # WHEN calling the event handler
     result = app({"path": "/my/path", "httpMethod": "GET", "headers": {}}, None)
@@ -287,7 +286,7 @@ def test_cache_control_200():
 
     @app.get("/success", cache_control="max-age=600")
     def with_cache_control() -> Response:
-        return Response(200, TEXT_HTML, "has 200 response")
+        return Response(200, content_types.TEXT_HTML, "has 200 response")
 
     def handler(event, context):
         return app.resolve(event, context)
@@ -298,7 +297,7 @@ def handler(event, context):
 
     # THEN return the set Cache-Control
     headers = result["headers"]
-    assert headers["Content-Type"] == TEXT_HTML
+    assert headers["Content-Type"] == content_types.TEXT_HTML
     assert headers["Cache-Control"] == "max-age=600"
 
 
@@ -308,7 +307,7 @@ def test_cache_control_non_200():
 
     @app.delete("/fails", cache_control="max-age=600")
     def with_cache_control_has_500() -> Response:
-        return Response(503, TEXT_HTML, "has 503 response")
+        return Response(503, content_types.TEXT_HTML, "has 503 response")
 
     def handler(event, context):
         return app.resolve(event, context)
@@ -319,7 +318,7 @@ def handler(event, context):
 
     # THEN return a Cache-Control of "no-cache"
     headers = result["headers"]
-    assert headers["Content-Type"] == TEXT_HTML
+    assert headers["Content-Type"] == content_types.TEXT_HTML
     assert headers["Cache-Control"] == "no-cache"
 
 
@@ -482,7 +481,7 @@ def test_custom_preflight_response():
     def custom_preflight():
         return Response(
             status_code=200,
-            content_type=TEXT_HTML,
+            content_type=content_types.TEXT_HTML,
             body="Foo",
             headers={"Access-Control-Allow-Methods": "CUSTOM"},
         )
@@ -498,7 +497,7 @@ def custom_method():
     assert result["statusCode"] == 200
     assert result["body"] == "Foo"
     headers = result["headers"]
-    assert headers["Content-Type"] == TEXT_HTML
+    assert headers["Content-Type"] == content_types.TEXT_HTML
     assert "Access-Control-Allow-Origin" in result["headers"]
     assert headers["Access-Control-Allow-Methods"] == "CUSTOM"
 
@@ -522,7 +521,7 @@ def raises_error():
     assert result["statusCode"] == 500
     assert "Traceback (most recent call last)" in result["body"]
     headers = result["headers"]
-    assert headers["Content-Type"] == "text/plain"
+    assert headers["Content-Type"] == content_types.TEXT_PLAIN
 
 
 def test_unhandled_exceptions_debug_off():

From 4beaaaa324887293ed81159dbac24de9af7e1bc9 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Tue, 6 Jul 2021 12:46:51 -0700
Subject: [PATCH 07/10] feat(api-gateway): print json event when debug is on

---
 .../event_handler/api_gateway.py              |   2 +
 .../event_handler/test_api_gateway.py         | 151 ++++++++++--------
 tests/functional/py.typed                     |   0
 3 files changed, 86 insertions(+), 67 deletions(-)
 create mode 100644 tests/functional/py.typed

diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
index 35457b1e05a..b6e9cd4698b 100644
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -434,6 +434,8 @@ def resolve(self, event, context) -> Dict[str, Any]:
         dict
             Returns the dict response
         """
+        if self._debug:
+            print(self._json_dump(event))
         self.current_event = self._to_proxy_event(event)
         self.lambda_context = context
         return self._resolve().build(self.current_event, self._cors)
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index 738ebd4794f..3d77aa7521c 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -1,9 +1,11 @@
 import base64
+import builtins
 import json
 import zlib
 from decimal import Decimal
 from pathlib import Path
 from typing import Dict
+from unittest.mock import MagicMock
 
 import pytest
 
@@ -502,73 +504,6 @@ def custom_method():
     assert headers["Access-Control-Allow-Methods"] == "CUSTOM"
 
 
-def test_unhandled_exceptions_debug_on():
-    # GIVEN debug is enabled
-    # AND an unhandled exception is raised
-    app = ApiGatewayResolver(debug=True)
-    assert app._debug
-
-    @app.get("/raises-error")
-    def raises_error():
-        raise RuntimeError("Foo")
-
-    # WHEN calling the handler
-    result = app({"path": "/raises-error", "httpMethod": "GET"}, None)
-
-    # THEN return a 500
-    # AND Content-Type is set to text/plain
-    # AND include the exception traceback in the response
-    assert result["statusCode"] == 500
-    assert "Traceback (most recent call last)" in result["body"]
-    headers = result["headers"]
-    assert headers["Content-Type"] == content_types.TEXT_PLAIN
-
-
-def test_unhandled_exceptions_debug_off():
-    # GIVEN debug is disabled
-    # AND an unhandled exception is raised
-    app = ApiGatewayResolver(debug=False)
-    assert not app._debug
-
-    @app.get("/raises-error")
-    def raises_error():
-        raise RuntimeError("Foo")
-
-    # WHEN calling the handler
-    # THEN raise the original exception
-    with pytest.raises(RuntimeError) as e:
-        app({"path": "/raises-error", "httpMethod": "GET"}, None)
-
-    # AND include the original error
-    assert e.value.args == ("Foo",)
-
-
-def test_debug_mode_environment_variable(monkeypatch):
-    # GIVEN a debug mode environment variable is set
-    monkeypatch.setenv(constants.EVENT_HANDLER_DEBUG_ENV, "true")
-    app = ApiGatewayResolver()
-
-    # WHEN calling app._debug
-    # THEN the debug mode is enabled
-    assert app._debug
-
-
-def test_debug_json_formatting():
-    # GIVEN debug is True
-    app = ApiGatewayResolver(debug=True)
-    response = {"message": "Foo"}
-
-    @app.get("/foo")
-    def foo():
-        return response
-
-    # WHEN calling the handler
-    result = app({"path": "/foo", "httpMethod": "GET"}, None)
-
-    # THEN return a pretty print json in the body
-    assert result["body"] == json.dumps(response, indent=4)
-
-
 def test_service_error_responses():
     # SCENARIO handling different kind of service errors being raised
     app = ApiGatewayResolver(cors=CORSConfig())
@@ -651,3 +586,85 @@ def service_error():
     assert "Access-Control-Allow-Origin" in result["headers"]
     expected = {"statusCode": 502, "message": "Something went wrong!"}
     assert result["body"] == json_dump(expected)
+
+
+def test_debug_unhandled_exceptions_debug_on():
+    # GIVEN debug is enabled
+    # AND an unhandled exception is raised
+    app = ApiGatewayResolver(debug=True)
+    assert app._debug
+
+    @app.get("/raises-error")
+    def raises_error():
+        raise RuntimeError("Foo")
+
+    # WHEN calling the handler
+    result = app({"path": "/raises-error", "httpMethod": "GET"}, None)
+
+    # THEN return a 500
+    # AND Content-Type is set to text/plain
+    # AND include the exception traceback in the response
+    assert result["statusCode"] == 500
+    assert "Traceback (most recent call last)" in result["body"]
+    headers = result["headers"]
+    assert headers["Content-Type"] == content_types.TEXT_PLAIN
+
+
+def test_debug_unhandled_exceptions_debug_off():
+    # GIVEN debug is disabled
+    # AND an unhandled exception is raised
+    app = ApiGatewayResolver(debug=False)
+    assert not app._debug
+
+    @app.get("/raises-error")
+    def raises_error():
+        raise RuntimeError("Foo")
+
+    # WHEN calling the handler
+    # THEN raise the original exception
+    with pytest.raises(RuntimeError) as e:
+        app({"path": "/raises-error", "httpMethod": "GET"}, None)
+
+    # AND include the original error
+    assert e.value.args == ("Foo",)
+
+
+def test_debug_mode_environment_variable(monkeypatch):
+    # GIVEN a debug mode environment variable is set
+    monkeypatch.setenv(constants.EVENT_HANDLER_DEBUG_ENV, "true")
+    app = ApiGatewayResolver()
+
+    # WHEN calling app._debug
+    # THEN the debug mode is enabled
+    assert app._debug
+
+
+def test_debug_json_formatting():
+    # GIVEN debug is True
+    app = ApiGatewayResolver(debug=True)
+    response = {"message": "Foo"}
+
+    @app.get("/foo")
+    def foo():
+        return response
+
+    # WHEN calling the handler
+    result = app({"path": "/foo", "httpMethod": "GET"}, None)
+
+    # THEN return a pretty print json in the body
+    assert result["body"] == json.dumps(response, indent=4)
+
+
+def test_debug_print_event(monkeypatch):
+    # GIVE debug is True
+    app = ApiGatewayResolver(debug=True)
+    mocked_print = MagicMock()
+    monkeypatch.setattr(builtins, "print", mocked_print)
+
+    # WHEN calling resolve
+    event = {"path": "/foo", "httpMethod": "GET"}
+    app(event, None)
+
+    # THEN print the event
+    # NOTE: other calls might have happened outside of this mock
+    mocked_print.assert_any_call(json.dumps(event, indent=4))
diff --git a/tests/functional/py.typed b/tests/functional/py.typed
new file mode 100644
index 00000000000..e69de29bb2d

From efa025f067838a1a65f90aa2efb6b74f01d93713 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Fri, 9 Jul 2021 01:16:22 -0700
Subject: [PATCH 08/10] refactor(constants): some cleanup

---
 aws_lambda_powertools/shared/constants.py | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py
index 05eac6cd8c4..8388eded654 100644
--- a/aws_lambda_powertools/shared/constants.py
+++ b/aws_lambda_powertools/shared/constants.py
@@ -10,13 +10,12 @@
 
 METRICS_NAMESPACE_ENV: str = "POWERTOOLS_METRICS_NAMESPACE"
 
+EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG"
+
 SAM_LOCAL_ENV: str = "AWS_SAM_LOCAL"
 CHALICE_LOCAL_ENV: str = "AWS_CHALICE_CLI_MODE"
 SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME"
 XRAY_TRACE_ID_ENV: str = "_X_AMZN_TRACE_ID"
 
-
-XRAY_SDK_MODULE = "aws_xray_sdk"
-XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core"
-
-EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG"
+XRAY_SDK_MODULE: str = "aws_xray_sdk"
+XRAY_SDK_CORE_MODULE: str = "aws_xray_sdk.core"

From e7df7d523030baaa02ad10d7e5cd18359e4a5858 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Fri, 9 Jul 2021 01:24:11 -0700
Subject: [PATCH 09/10] tests: use capsys

Co-authored-by: Heitor Lessa <heitor.lessa@hotmail.com>
---
 tests/functional/event_handler/test_api_gateway.py | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index 3d77aa7521c..b2ecf14f441 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -655,16 +655,14 @@ def foo():
     assert result["body"] == json.dumps(response, indent=4)
 
 
-def test_debug_print_event(monkeypatch):
+def test_debug_print_event(capsys):
     # GIVE debug is True
     app = ApiGatewayResolver(debug=True)
-    mocked_print = MagicMock()
-    monkeypatch.setattr(builtins, "print", mocked_print)
 
     # WHEN calling resolve
     event = {"path": "/foo", "httpMethod": "GET"}
     app(event, None)
 
     # THEN print the event
-    # NOTE: other calls might have happened outside of this mock
-    mocked_print.assert_any_call(json.dumps(event, indent=4))
+    out, err = capsys.readouterr()
+    assert json.loads(out) == event

From f1e6faa5345332a765a06e6b46badb11f7f62002 Mon Sep 17 00:00:00 2001
From: Michael Brewer <michael.brewer@gyft.com>
Date: Fri, 9 Jul 2021 01:27:49 -0700
Subject: [PATCH 10/10] chore: remove unused imports

---
 tests/functional/event_handler/test_api_gateway.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
index b2ecf14f441..b39dccc6084 100644
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -1,11 +1,9 @@
 import base64
-import builtins
 import json
 import zlib
 from decimal import Decimal
 from pathlib import Path
 from typing import Dict
-from unittest.mock import MagicMock
 
 import pytest