diff --git a/README.md b/README.md index 0ecb094e7..b7285914c 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ to change Zappa's behavior. Use these at your own risk! { "dev": { "aws_region": "us-east-1", // AWS Region (default US East), + "debug": true // Print Zappa configuration errors tracebacks in the 500 + "delete_zip": true // Delete the local zip archive after code updates "domain": "yourapp.yourdomain.com", // Required if you're using a domain "http_methods": ["GET", "POST"], // HTTP Methods to route, "integration_response_codes": [200, 301, 404, 500], // Integration response status codes to route diff --git a/test_settings.json b/test_settings.json index 61a2e0db9..f9342c728 100644 --- a/test_settings.json +++ b/test_settings.json @@ -2,6 +2,7 @@ "ttt333": { "s3_bucket": "lmbda", "app_function": "tests.test_app.hello_world", - "delete_zip": true + "delete_zip": true, + "debug": true } } diff --git a/test_settings.py b/test_settings.py index 0a8a02a9b..e43b96003 100644 --- a/test_settings.py +++ b/test_settings.py @@ -1,3 +1,4 @@ APP_MODULE = 'tests.test_app' APP_FUNCTION = 'hello_world' -SCRIPT_NAME = 'hello_world' \ No newline at end of file +DEBUG = 'True' +SCRIPT_NAME = 'hello_world' diff --git a/zappa/cli.py b/zappa/cli.py index 4727da341..2d835eedb 100644 --- a/zappa/cli.py +++ b/zappa/cli.py @@ -48,6 +48,7 @@ class ZappaCLI(object): api_stage = None app_function = None aws_region = None + debug = None project_name = None lambda_name = None s3_bucket_name = None @@ -283,6 +284,8 @@ def load_settings(self, settings_file="zappa_settings.json", session=None): self.api_stage].get('app_function', None) self.aws_region = self.zappa_settings[ self.api_stage].get('aws_region', 'us-east-1') + self.debug = self.zappa_settings[ + self.api_stage].get('debug', True) # Create an Zappa object.. self.zappa = Zappa(session) @@ -326,6 +329,9 @@ def create_package(self): app_module, app_function = self.app_function.rsplit('.', 1) settings_s = "# Generated by Zappa\nAPP_MODULE='%s'\nAPP_FUNCTION='%s'\n" % (app_module, app_function) + if self.debug is not None: + settings_s = settings_s + "DEBUG='%s'" % (self.debug) # Cast to Bool in handler + # Lambda requires a specific chmod temp_settings = tempfile.NamedTemporaryFile(delete=False) os.chmod(temp_settings.name, 0644) diff --git a/zappa/handler.py b/zappa/handler.py index b6893cf78..dec6772dd 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -5,6 +5,7 @@ import importlib import logging import os +import traceback from urllib import urlencode from StringIO import StringIO @@ -24,6 +25,9 @@ logger = logging.getLogger() logger.setLevel(logging.INFO) +class LambdaException(Exception): + pass + class LambdaHandler(object): """ Singleton for avoiding duplicate setup. @@ -58,80 +62,100 @@ def handler(self, event, context): """ - time_start = datetime.datetime.now() - - settings = self.settings - - # The app module - app_module = importlib.import_module(settings.APP_MODULE) - - # The application - app_function = getattr(app_module, settings.APP_FUNCTION) - - app = ZappaWSGIMiddleware(app_function) - - # This is a normal HTTP request - if event.get('method', None): - # If we just want to inspect this, - # return this event instead of processing the request - # https://your_api.aws-api.com/?event_echo=true - event_echo = getattr(settings, "EVENT_ECHO", True) - if event_echo: - if 'event_echo' in list(event['params'].values()): - return {'Content': str(event) + '\n' + str(context), 'Status': 200} - - # Create the environment for WSGI and handle the request - environ = create_wsgi_request(event, script_name='', - trailing_slash=False) - - # We are always on https on Lambda, so tell our wsgi app that. - environ['wsgi.url_scheme'] = 'https' - - response = Response.from_app(app, environ) - - zappa_returndict = dict() - - if response.data: - zappa_returndict['Content'] = response.data - - # Pack the WSGI response into our special dictionary. - for (header_name, header_value) in response.headers: - zappa_returndict[header_name] = header_value - zappa_returndict['Status'] = response.status_code - - # To ensure correct status codes, we need to - # pack the response as a deterministic B64 string and raise it - # as an error to match our APIGW regex. - # The DOCTYPE ensures that the page still renders in the browser. - exception = None - if response.status_code in [400, 401, 403, 404, 500]: - content = "" + str(response.status_code) + response.data + try: + time_start = datetime.datetime.now() + + settings = self.settings + + # The app module + app_module = importlib.import_module(settings.APP_MODULE) + + # The application + app_function = getattr(app_module, settings.APP_FUNCTION) + + app = ZappaWSGIMiddleware(app_function) + + # This is a normal HTTP request + if event.get('method', None): + # If we just want to inspect this, + # return this event instead of processing the request + # https://your_api.aws-api.com/?event_echo=true + event_echo = getattr(settings, "EVENT_ECHO", True) + if event_echo: + if 'event_echo' in list(event['params'].values()): + return {'Content': str(event) + '\n' + str(context), 'Status': 200} + + # Create the environment for WSGI and handle the request + environ = create_wsgi_request(event, script_name='', + trailing_slash=False) + + # We are always on https on Lambda, so tell our wsgi app that. + environ['wsgi.url_scheme'] = 'https' + + # Execute the application + response = Response.from_app(app, environ) + + # This is the object we're going to return. + zappa_returndict = dict() + + if response.data: + zappa_returndict['Content'] = response.data + + # Pack the WSGI response into our special dictionary. + for (header_name, header_value) in response.headers: + zappa_returndict[header_name] = header_value + zappa_returndict['Status'] = response.status_code + + # To ensure correct status codes, we need to + # pack the response as a deterministic B64 string and raise it + # as an error to match our APIGW regex. + # The DOCTYPE ensures that the page still renders in the browser. + exception = None + if response.status_code in [400, 401, 403, 404, 500]: + content = "" + str(response.status_code) + response.data + exception = base64.b64encode(content) + # Internal are changed to become relative redirects + # so they still work for apps on raw APIGW and on a domain. + elif response.status_code in [301, 302]: + # Location is by default relative on Flask. Location is by default + # absolute on Werkzeug. We can set autocorrect_location_header on + # the response to False, but it doesn't work. We have to manually + # remove the host part. + location = response.location + hostname = 'https://' + environ['HTTP_HOST'] + if location.startswith(hostname): + exception = location[len(hostname):] + + # 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) + + # Finally, return the response to API Gateway. + if exception: # pragma: no cover + raise LambdaException(exception) + else: + return zappa_returndict + except LambdaException as e: # pragma: nocover + raise e + except Exception as e: # pragma: nocover + + # Print statements are visible in the logs either way + print(e) + + # Print the error to the browser upon failure? + debug = bool(getattr(app_module, settings.DEBUG, True)) + if debug: + # Return this unspecified exception as a 500. + content = "500. From Zappa:
" + str(e) + "
" + traceback.format_exc().replace('\n', '" exception = base64.b64encode(content) - # Internal are changed to become relative redirects - # so they still work for apps on raw APIGW and on a domain. - elif response.status_code in [301, 302]: - # Location is by default relative on Flask. Location is by default - # absolute on Werkzeug. We can set autocorrect_location_header on - # the response to False, but it doesn't work. We have to manually - # remove the host part. - location = response.location - hostname = 'https://' + environ['HTTP_HOST'] - if location.startswith(hostname): - exception = location[len(hostname):] - - # 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) - - # Finally, return the response to API Gateway. - if exception: # pragma: no cover raise Exception(exception) else: - return zappa_returndict + raise e + def lambda_handler(event, context): # pragma: no cover return LambdaHandler.lambda_handler(event, context)
') + "