Repository: Miserlou/Zappa

Issue:
Zappa always leaks the underlying WSGI response
<!--- Provide a general summary of the issue in the Title above -->
## Context

From the WSGI spec:

>f the iterable returned by the application has a close() method, the server or gateway must call that method upon completion of the current request, whether the request was completed normally, or terminated early due to an error (this is to support resource release by the application).

Zappa creates a `werkzeug.Response` object but never closes it. That might cause the leak of an arbitrary amount of resources that the WSGI app has decided to only close when the response is closed. For example the file that is currently being streamed to the client, a database connection or even a HTTP request that is proxied through the WSGI app.

It is surprising that this issue was never found despite the popularity of Zappa. My guess is that AWS kills and restarts a function when it notices the function to run out of file descriptors.

This issue affects every Python version.

## Possible Fix

The following fix is sufficient for successful responses, but it probably should be called in a `try: .. finally: ..` or `with` block like you would do with files:

```diff
diff --git a/zappa/handler.py b/zappa/handler.py
index 5bfcc43..00e8d3e 100644
--- a/zappa/handler.py
+++ b/zappa/handler.py
@@ -540,6 +540,8 @@ class LambdaHandler(object):
                 response.content = response.data
                 common_log(environ, response, response_time=response_time_ms)

+                response.close()
+
                 return zappa_returndict
         except Exception as e:  # pragma: no cover
```

A better one would be to wrap everything including the `return zappa_returndict` in  a `with Response.from_app(..) as response:`

<!--- Not obligatory, but suggest a fix or reason for the bug -->

## Steps to Reproduce

This is how this issue was surfaced:

1. Install `sentry-sdk` and set up its [aws-lambda](https://docs.sentry.io/platforms/python/aws_lambda/) integration together with e.g. the [flask](https://docs.sentry.io/platforms/python/flask/) integration
2. deploy your flask app with zappa
3. encounter https://github.com/getsentry/sentry-python/issues/190

## Your Environment

* Zappa version used: 0.46.1, as well as master at https://github.com/Miserlou/Zappa/commit/7730144dec01b4cdbf0205b936a200e54a287ebb


Discussion:

Resolving patch:
diff --git a/zappa/handler.py b/zappa/handler.py
--- a/zappa/handler.py
+++ b/zappa/handler.py
@@ -515,37 +515,36 @@ def handler(self, event, context):
                 environ['lambda.event'] = event
 
                 # Execute the application
-                response = Response.from_app(self.wsgi_app, environ)
-
-                # This is the object we're going to return.
-                # Pack the WSGI response into our special dictionary.
-                zappa_returndict = dict()
-
-                if response.data:
-                    if settings.BINARY_SUPPORT:
-                        if not response.mimetype.startswith("text/") \
-                            or response.mimetype != "application/json":
-                                zappa_returndict['body'] = base64.b64encode(response.data).decode('utf-8')
-                                zappa_returndict["isBase64Encoded"] = True
+                with Response.from_app(self.wsgi_app, environ) as response:
+                    # This is the object we're going to return.
+                    # Pack the WSGI response into our special dictionary.
+                    zappa_returndict = dict()
+
+                    if response.data:
+                        if settings.BINARY_SUPPORT:
+                            if not response.mimetype.startswith("text/") \
+                                or response.mimetype != "application/json":
+                                    zappa_returndict['body'] = base64.b64encode(response.data).decode('utf-8')
+                                    zappa_returndict["isBase64Encoded"] = True
+                            else:
+                                zappa_returndict['body'] = response.data
                         else:
-                            zappa_returndict['body'] = response.data
-                    else:
-                        zappa_returndict['body'] = response.get_data(as_text=True)
-
-                zappa_returndict['statusCode'] = response.status_code
-                zappa_returndict['headers'] = {}
-                for key, value in response.headers:
-                    zappa_returndict['headers'][key] = value
-
-                # Calculate the total response time,
-                # and log it in the Common Log format.
-                time_end = datetime.datetime.now()
-                delta = time_end - time_start
-                response_time_ms = delta.total_seconds() * 1000
-                response.content = response.data
-                common_log(environ, response, response_time=response_time_ms)
-
-                return zappa_returndict
+                            zappa_returndict['body'] = response.get_data(as_text=True)
+
+                    zappa_returndict['statusCode'] = response.status_code
+                    zappa_returndict['headers'] = {}
+                    for key, value in response.headers:
+                        zappa_returndict['headers'][key] = value
+
+                    # Calculate the total response time,
+                    # and log it in the Common Log format.
+                    time_end = datetime.datetime.now()
+                    delta = time_end - time_start
+                    response_time_ms = delta.total_seconds() * 1000
+                    response.content = response.data
+                    common_log(environ, response, response_time=response_time_ms)
+
+                    return zappa_returndict
         except Exception as e:  # pragma: no cover
 
             # Print statements are visible in the logs either way


Test patch: