• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zappa / Zappa / 15133616794

20 May 2025 09:12AM UTC coverage: 74.696% (-0.2%) from 74.885%
15133616794

push

github

web-flow
fixed conflict(Feature: Handle V2 Event #1182) (#1377)

* :wrench: migrate https://github.com/zappa/Zappa/pull/971 to lastest master

* :art: run black/isort

* :recycle: refactor to allow for other binary ignore types based on mimetype. (currently openapi schema can't be passed as text.

* :art: run black/fix flake8

* :wrench: add EXCEPTION_HANDLER setting

* :bug: fix zappa_returndict["body"] assignment

* :pencil: add temp debug info

* :fire: delete unnecessary print statements

* :recycle: Update comments and minor refactor for clarity

* :recycle: refactor for ease of testing and clarity

* :art: fix flake8

* :sparkles: add `additional_text_mimetypes` setting
:white_check_mark: add testcases for additional_text_mimetypes handling

* :wrench: Expand default text mimetypes mentioned in https://github.com/zappa/Zappa/pull/1023
:recycle: define "DEFAULT_TEXT_MIMETYPES" and move to utilities.py

* :art: run black/isort

* :art: run black/isort

* feat: implement handler for event with format  version 2.0

* refactor: getting processed response body from new method

* fix: lint error

* chore: move variable initialization before initial if condition

* refactor: abstract implementations to two processing methods

* fix: determine payload version based on value in the event itself

* fixed lint

---------

Co-authored-by: monkut <shane.cousins@gmail.com>
Co-authored-by: Rehan Hawari <rehan.hawari10@gmail.com>

72 of 97 new or added lines in 2 files covered. (74.23%)

4 existing lines in 2 files now uncovered.

2822 of 3778 relevant lines covered (74.7%)

3.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.21
/zappa/handler.py
1
import base64
5✔
2
import collections
5✔
3
import datetime
5✔
4
import importlib
5✔
5
import inspect
5✔
6
import json
5✔
7
import logging
5✔
8
import os
5✔
9
import sys
5✔
10
import tarfile
5✔
11
import traceback
5✔
12
from builtins import str
5✔
13
from types import ModuleType
5✔
14
from typing import Tuple
5✔
15

16
import boto3
5✔
17
from werkzeug.wrappers import Response
5✔
18

19
# This file may be copied into a project's root,
20
# so handle both scenarios.
21
try:
5✔
22
    from zappa.middleware import ZappaWSGIMiddleware
5✔
23
    from zappa.utilities import DEFAULT_TEXT_MIMETYPES, merge_headers, parse_s3_url
5✔
24
    from zappa.wsgi import common_log, create_wsgi_request
5✔
25
except ImportError:  # pragma: no cover
26
    from .middleware import ZappaWSGIMiddleware
27
    from .utilities import DEFAULT_TEXT_MIMETYPES, merge_headers, parse_s3_url
28
    from .wsgi import common_log, create_wsgi_request
29

30

31
# Set up logging
32
logging.basicConfig()
5✔
33
logger = logging.getLogger(__name__)
5✔
34
logger.setLevel(logging.INFO)
5✔
35

36

37
class LambdaHandler:
5✔
38
    """
39
    Singleton for avoiding duplicate setup.
40

41
    Pattern provided by @benbangert.
42
    """
43

44
    __instance = None
5✔
45
    settings = None
5✔
46
    settings_name = None
5✔
47
    session = None
5✔
48

49
    # Application
50
    app_module = None
5✔
51
    wsgi_app = None
5✔
52
    trailing_slash = False
5✔
53

54
    def __new__(cls, settings_name="zappa_settings", session=None):
5✔
55
        """Singleton instance to avoid repeat setup"""
56
        if LambdaHandler.__instance is None:
5✔
57
            print("Instancing..")
5✔
58
            LambdaHandler.__instance = object.__new__(cls)
5✔
59
        return LambdaHandler.__instance
5✔
60

61
    def __init__(self, settings_name="zappa_settings", session=None):
5✔
62
        # We haven't cached our settings yet, load the settings and app.
63
        if not self.settings:
5✔
64
            # Loading settings from a python module
65
            self.settings = importlib.import_module(settings_name)
5✔
66
            self.settings_name = settings_name
5✔
67
            self.session = session
5✔
68

69
            # Custom log level
70
            if self.settings.LOG_LEVEL:
5✔
71
                level = logging.getLevelName(self.settings.LOG_LEVEL)
5✔
72
                logger.setLevel(level)
5✔
73

74
            remote_env = getattr(self.settings, "REMOTE_ENV", None)
5✔
75
            remote_bucket, remote_file = parse_s3_url(remote_env)
5✔
76

77
            if remote_bucket and remote_file:
5✔
78
                self.load_remote_settings(remote_bucket, remote_file)
5✔
79

80
            # Let the system know that this will be a Lambda/Zappa/Stack
81
            os.environ["SERVERTYPE"] = "AWS Lambda"
5✔
82
            os.environ["FRAMEWORK"] = "Zappa"
5✔
83
            try:
5✔
84
                os.environ["PROJECT"] = self.settings.PROJECT_NAME
5✔
85
                os.environ["STAGE"] = self.settings.API_STAGE
5✔
86
            except Exception:  # pragma: no cover
87
                pass
88

89
            # Set any locally defined env vars
90
            # Environment variable keys can't be Unicode
91
            # https://github.com/Miserlou/Zappa/issues/604
92
            for key in self.settings.ENVIRONMENT_VARIABLES.keys():
5✔
93
                os.environ[str(key)] = self.settings.ENVIRONMENT_VARIABLES[key]
5✔
94

95
            # Pulling from S3 if given a zip path
96
            project_archive_path = getattr(self.settings, "ARCHIVE_PATH", None)
5✔
97
            if project_archive_path:
5✔
98
                self.load_remote_project_archive(project_archive_path)
×
99

100
            # Load compiled library to the PythonPath
101
            # checks if we are the slim_handler since this is not needed otherwise
102
            # https://github.com/Miserlou/Zappa/issues/776
103
            is_slim_handler = getattr(self.settings, "SLIM_HANDLER", False)
5✔
104
            if is_slim_handler:
5✔
105
                included_libraries = getattr(self.settings, "INCLUDE", [])
×
106
                try:
×
107
                    from ctypes import cdll
×
108

109
                    for library in included_libraries:
×
110
                        try:
×
111
                            cdll.LoadLibrary(os.path.join(os.getcwd(), library))
×
112
                        except OSError:
×
113
                            print("Failed to find library: {}...right filename?".format(library))
×
114
                except ImportError:
×
115
                    print("Failed to import cytpes library")
×
116

117
            # This is a non-WSGI application
118
            # https://github.com/Miserlou/Zappa/pull/748
119
            if not hasattr(self.settings, "APP_MODULE") and not self.settings.DJANGO_SETTINGS:
5✔
120
                self.app_module = None
×
121
                wsgi_app_function = None
×
122
            # This is probably a normal WSGI app (Or django with overloaded wsgi application)
123
            # https://github.com/Miserlou/Zappa/issues/1164
124
            elif hasattr(self.settings, "APP_MODULE"):
5✔
125
                if self.settings.DJANGO_SETTINGS:
5✔
126
                    sys.path.append("/var/task")
×
127
                    from django.conf import (
×
128
                        ENVIRONMENT_VARIABLE as SETTINGS_ENVIRONMENT_VARIABLE,
129
                    )
130

131
                    # add the Lambda root path into the sys.path
132
                    self.trailing_slash = True
×
133
                    os.environ[SETTINGS_ENVIRONMENT_VARIABLE] = self.settings.DJANGO_SETTINGS
×
134
                else:
135
                    self.trailing_slash = False
5✔
136

137
                # The app module
138
                self.app_module = importlib.import_module(self.settings.APP_MODULE)
5✔
139

140
                # The application
141
                wsgi_app_function = getattr(self.app_module, self.settings.APP_FUNCTION)
5✔
142
            # Django gets special treatment.
143
            else:
144
                try:  # Support both for tests
×
145
                    from zappa.ext.django_zappa import get_django_wsgi
×
146
                except ImportError:  # pragma: no cover
147
                    from django_zappa_app import get_django_wsgi
148

149
                # Get the Django WSGI app from our extension
150
                wsgi_app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS)
×
151
                self.trailing_slash = True
×
152

153
            self.wsgi_app = ZappaWSGIMiddleware(wsgi_app_function)
5✔
154

155
    def load_remote_project_archive(self, project_zip_path):
5✔
156
        """
157
        Puts the project files from S3 in /tmp and adds to path
158
        """
159
        project_folder = "/tmp/{0!s}".format(self.settings.PROJECT_NAME)
×
160
        if not os.path.isdir(project_folder):
×
161
            # The project folder doesn't exist in this cold lambda, get it from S3
162
            if not self.session:
×
163
                boto_session = boto3.Session()
×
164
            else:
165
                boto_session = self.session
×
166

167
            # Download zip file from S3
168
            remote_bucket, remote_file = parse_s3_url(project_zip_path)
×
169
            s3 = boto_session.resource("s3")
×
170
            archive_on_s3 = s3.Object(remote_bucket, remote_file).get()
×
171

172
            with tarfile.open(fileobj=archive_on_s3["Body"], mode="r|gz") as t:
×
173
                t.extractall(project_folder)
×
174

175
        # Add to project path
176
        sys.path.insert(0, project_folder)
×
177

178
        # Change working directory to project folder
179
        # Related: https://github.com/Miserlou/Zappa/issues/702
180
        os.chdir(project_folder)
×
181
        return True
×
182

183
    def load_remote_settings(self, remote_bucket, remote_file):
5✔
184
        """
185
        Attempt to read a file from s3 containing a flat json object. Adds each
186
        key->value pair as environment variables. Helpful for keeping
187
        sensitiZve or stage-specific configuration variables in s3 instead of
188
        version control.
189
        """
190
        if not self.session:
5✔
191
            boto_session = boto3.Session()
×
192
        else:
193
            boto_session = self.session
5✔
194

195
        s3 = boto_session.resource("s3")
5✔
196
        try:
5✔
197
            remote_env_object = s3.Object(remote_bucket, remote_file).get()
5✔
198
        except Exception as e:  # pragma: no cover
199
            # catch everything aws might decide to raise
200
            print("Could not load remote settings file.", e)
201
            return
202

203
        try:
5✔
204
            content = remote_env_object["Body"].read()
5✔
205
        except Exception as e:  # pragma: no cover
206
            # catch everything aws might decide to raise
207
            print("Exception while reading remote settings file.", e)
208
            return
209

210
        try:
5✔
211
            settings_dict = json.loads(content)
5✔
212
        except (ValueError, TypeError):  # pragma: no cover
213
            print("Failed to parse remote settings!")
214
            return
215

216
        # add each key-value to environment - overwrites existing keys!
217
        for key, value in settings_dict.items():
5✔
218
            if self.settings.LOG_LEVEL == "DEBUG":
5✔
219
                print("Adding {} -> {} to environment".format(key, value))
5✔
220
            # Environment variable keys can't be Unicode
221
            # https://github.com/Miserlou/Zappa/issues/604
222
            try:
5✔
223
                os.environ[str(key)] = value
5✔
224
            except Exception:
×
225
                if self.settings.LOG_LEVEL == "DEBUG":
×
226
                    print("Environment variable keys must be non-unicode!")
×
227

228
    @staticmethod
5✔
229
    def import_module_and_get_function(whole_function):
5✔
230
        """
231
        Given a modular path to a function, import that module
232
        and return the function.
233
        """
234
        module, function = whole_function.rsplit(".", 1)
5✔
235
        app_module = importlib.import_module(module)
5✔
236
        app_function = getattr(app_module, function)
5✔
237
        return app_function
5✔
238

239
    @classmethod
1✔
240
    def lambda_handler(cls, event, context):  # pragma: no cover
241
        handler = global_handler or cls()
242
        exception_handler = handler.settings.EXCEPTION_HANDLER
243
        try:
244
            return handler.handler(event, context)
245
        except Exception as ex:
246
            exception_processed = cls._process_exception(
247
                exception_handler=exception_handler,
248
                event=event,
249
                context=context,
250
                exception=ex,
251
            )
252
            if not exception_processed:
253
                # Only re-raise exception if handler directed so. Allows handler to control if lambda has to retry
254
                # an event execution in case of failure.
255
                raise
256

257
    @classmethod
5✔
258
    def _process_exception(cls, exception_handler, event, context, exception):
5✔
259
        exception_processed = False
5✔
260
        if exception_handler:
5✔
261
            try:
5✔
262
                handler_function = cls.import_module_and_get_function(exception_handler)
5✔
263
                exception_processed = handler_function(exception, event, context)
5✔
264
            except Exception as cex:
×
265
                logger.error(msg="Failed to process exception via custom handler.")
×
266
                print(cex)
×
267
        return exception_processed
5✔
268

269
    @staticmethod
5✔
270
    def _process_response_body(response: Response, settings: ModuleType) -> Tuple[str, bool]:
5✔
271
        """
272
        Perform Response body encoding/decoding
273

274
        Related: https://github.com/zappa/Zappa/issues/908
275
        API Gateway requires binary data be base64 encoded:
276
        https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis/
277
        When BINARY_SUPPORT is enabled the body is base64 encoded in the following cases:
278

279
        - Content-Encoding defined, commonly used to specify compression (br/gzip/deflate/etc)
280
          https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
281
          Content like this must be transmitted as b64.
282

283
        - Response assumed binary when Response.mimetype does
284
          not start with an entry defined in 'handle_as_text_mimetypes'
285
        """
286
        encode_body_as_base64 = False
5✔
287
        if settings.BINARY_SUPPORT:
5✔
288
            handle_as_text_mimetypes = DEFAULT_TEXT_MIMETYPES
5✔
289
            additional_text_mimetypes = getattr(settings, "ADDITIONAL_TEXT_MIMETYPES", None)
5✔
290
            if additional_text_mimetypes:
5✔
291
                handle_as_text_mimetypes += tuple(additional_text_mimetypes)
5✔
292

293
            if response.headers.get("Content-Encoding"):  # Assume br/gzip/deflate/etc encoding
5✔
294
                encode_body_as_base64 = True
5✔
295

296
            # werkzeug Response.mimetype: lowercase without parameters
297
            # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.mimetype
298
            elif not response.mimetype.startswith(handle_as_text_mimetypes):
5✔
299
                encode_body_as_base64 = True
5✔
300

301
        if encode_body_as_base64:
5✔
302
            body = base64.b64encode(response.data).decode("utf8")
5✔
303
        else:
304
            # response.data decoded by werkzeug
305
            # https://werkzeug.palletsprojects.com/en/2.2.x/wrappers/#werkzeug.wrappers.Request.get_data
306
            body = response.get_data(as_text=True)
5✔
307

308
        return body, encode_body_as_base64
5✔
309

310
    @staticmethod
5✔
311
    def run_function(app_function, event, context):
5✔
312
        """
313
        Given a function and event context,
314
        detect signature and execute, returning any result.
315
        """
316
        # getargspec does not support python 3 method with type hints
317
        # Related issue: https://github.com/Miserlou/Zappa/issues/1452
318
        if hasattr(inspect, "getfullargspec"):  # Python 3
5✔
319
            args, varargs, keywords, defaults, _, _, _ = inspect.getfullargspec(app_function)
5✔
320
        else:  # Python 2
321
            args, varargs, keywords, defaults = inspect.getargspec(app_function)
×
322
        num_args = len(args)
5✔
323
        if num_args == 0:
5✔
324
            result = app_function(event, context) if varargs else app_function()
5✔
325
        elif num_args == 1:
5✔
326
            result = app_function(event, context) if varargs else app_function(event)
5✔
327
        elif num_args == 2:
5✔
328
            result = app_function(event, context)
5✔
329
        else:
330
            raise RuntimeError(
5✔
331
                "Function signature is invalid. Expected a function that accepts at most " "2 arguments or varargs."
332
            )
333
        return result
5✔
334

335
    def get_function_for_aws_event(self, record):
5✔
336
        """
337
        Get the associated function to execute for a triggered AWS event
338

339
        Support S3, SNS, DynamoDB, kinesis and SQS events
340
        """
341
        if "s3" in record:
5✔
342
            if ":" in record["s3"]["configurationId"]:
5✔
343
                return record["s3"]["configurationId"].split(":")[-1]
5✔
344

345
        arn = None
5✔
346
        if "Sns" in record:
5✔
347
            try:
5✔
348
                message = json.loads(record["Sns"]["Message"])
5✔
349
                if message.get("command"):
5✔
350
                    return message["command"]
5✔
351
            except ValueError:
5✔
352
                pass
5✔
353
            arn = record["Sns"].get("TopicArn")
5✔
354
        elif "dynamodb" in record or "kinesis" in record:
5✔
355
            arn = record.get("eventSourceARN")
5✔
356
        elif "eventSource" in record and record.get("eventSource") == "aws:sqs":
5✔
357
            arn = record.get("eventSourceARN")
5✔
358
        elif "s3" in record:
×
359
            arn = record["s3"]["bucket"]["arn"]
×
360

361
        if arn:
5✔
362
            return self.settings.AWS_EVENT_MAPPING.get(arn)
5✔
363

364
        return None
×
365

366
    def get_function_from_bot_intent_trigger(self, event):
5✔
367
        """
368
        For the given event build ARN and return the configured function
369
        """
370
        intent = event.get("currentIntent")
5✔
371
        if intent:
5✔
372
            intent = intent.get("name")
5✔
373
            if intent:
5✔
374
                return self.settings.AWS_BOT_EVENT_MAPPING.get("{}:{}".format(intent, event.get("invocationSource")))
5✔
375

376
    def get_function_for_cognito_trigger(self, trigger):
5✔
377
        """
378
        Get the associated function to execute for a cognito trigger
379
        """
380
        print(
5✔
381
            "get_function_for_cognito_trigger",
382
            self.settings.COGNITO_TRIGGER_MAPPING,
383
            trigger,
384
            self.settings.COGNITO_TRIGGER_MAPPING.get(trigger),
385
        )
386
        return self.settings.COGNITO_TRIGGER_MAPPING.get(trigger)
5✔
387

388
    def handler(self, event, context):
5✔
389
        """
390
        An AWS Lambda function which parses specific API Gateway input into a
391
        WSGI request, feeds it to our WSGI app, processes the response, and returns
392
        that back to the API Gateway.
393

394
        """
395
        settings = self.settings
5✔
396

397
        # If in DEBUG mode, log all raw incoming events.
398
        if settings.DEBUG:
5✔
399
            logger.debug("Zappa Event: {}".format(event))
5✔
400

401
        # Set any API Gateway defined Stage Variables
402
        # as env vars
403
        if event.get("stageVariables"):
5✔
404
            for key in event["stageVariables"].keys():
×
405
                os.environ[str(key)] = event["stageVariables"][key]
×
406

407
        # This is the result of a keep alive, recertify
408
        # or scheduled event.
409
        if event.get("detail-type") == "Scheduled Event":
5✔
410
            whole_function = event["resources"][0].split("/")[-1].split("-")[-1]
5✔
411

412
            # This is a scheduled function.
413
            if "." in whole_function:
5✔
414
                app_function = self.import_module_and_get_function(whole_function)
5✔
415

416
                # Execute the function!
417
                return self.run_function(app_function, event, context)
5✔
418

419
            # Else, let this execute as it were.
420

421
        # This is a direct command invocation.
422
        elif event.get("command", None):
5✔
423
            whole_function = event["command"]
5✔
424
            app_function = self.import_module_and_get_function(whole_function)
5✔
425
            result = self.run_function(app_function, event, context)
5✔
426
            print("Result of %s:" % whole_function)
5✔
427
            print(result)
5✔
428
            return result
5✔
429

430
        # This is a direct, raw python invocation.
431
        # It's _extremely_ important we don't allow this event source
432
        # to be overridden by unsanitized, non-admin user input.
433
        elif event.get("raw_command", None):
5✔
434
            raw_command = event["raw_command"]
5✔
435
            exec(raw_command)
5✔
436
            return
5✔
437

438
        # This is a Django management command invocation.
439
        elif event.get("manage", None):
5✔
440
            from django.core import management
×
441

442
            try:  # Support both for tests
×
443
                from zappa.ext.django_zappa import get_django_wsgi
×
444
            except ImportError:  # pragma: no cover
445
                from django_zappa_app import get_django_wsgi
446

447
            # Get the Django WSGI app from our extension
448
            # We don't actually need the function,
449
            # but we do need to do all of the required setup for it.
450
            app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS)
×
451

452
            # Couldn't figure out how to get the value into stdout with StringIO..
453
            # Read the log for now. :[]
454
            management.call_command(*event["manage"].split(" "))
×
455
            return {}
×
456

457
        # This is an AWS-event triggered invocation.
458
        elif event.get("Records", None):
5✔
459
            records = event.get("Records")
5✔
460
            result = None
5✔
461
            whole_function = self.get_function_for_aws_event(records[0])
5✔
462
            if whole_function:
5✔
463
                app_function = self.import_module_and_get_function(whole_function)
5✔
464
                result = self.run_function(app_function, event, context)
5✔
465
                logger.debug(result)
5✔
466
            else:
467
                logger.error("Cannot find a function to process the triggered event.")
5✔
468
            return result
5✔
469

470
        # this is an AWS-event triggered from Lex bot's intent
471
        elif event.get("bot"):
5✔
472
            result = None
5✔
473
            whole_function = self.get_function_from_bot_intent_trigger(event)
5✔
474
            if whole_function:
5✔
475
                app_function = self.import_module_and_get_function(whole_function)
5✔
476
                result = self.run_function(app_function, event, context)
5✔
477
                logger.debug(result)
5✔
478
            else:
479
                logger.error("Cannot find a function to process the triggered event.")
×
480
            return result
5✔
481

482
        # This is an API Gateway authorizer event
483
        elif event.get("type") == "TOKEN":
5✔
484
            whole_function = self.settings.AUTHORIZER_FUNCTION
5✔
485
            if whole_function:
5✔
486
                app_function = self.import_module_and_get_function(whole_function)
5✔
487
                policy = self.run_function(app_function, event, context)
5✔
488
                return policy
5✔
489
            else:
490
                logger.error("Cannot find a function to process the authorization request.")
5✔
491
                raise Exception("Unauthorized")
5✔
492

493
        # This is an AWS Cognito Trigger Event
494
        elif event.get("triggerSource", None):
5✔
495
            triggerSource = event.get("triggerSource")
5✔
496
            whole_function = self.get_function_for_cognito_trigger(triggerSource)
5✔
497
            result = event
5✔
498
            if whole_function:
5✔
499
                app_function = self.import_module_and_get_function(whole_function)
×
500
                result = self.run_function(app_function, event, context)
×
501
                logger.debug(result)
×
502
            else:
503
                logger.error("Cannot find a function to handle cognito trigger {}".format(triggerSource))
5✔
504
            return result
5✔
505

506
        # This is a CloudWatch event
507
        # Related: https://github.com/Miserlou/Zappa/issues/1924
508
        elif event.get("awslogs", None):
5✔
509
            result = None
5✔
510
            whole_function = "{}.{}".format(settings.APP_MODULE, settings.APP_FUNCTION)
5✔
511
            app_function = self.import_module_and_get_function(whole_function)
5✔
512
            if app_function:
5✔
513
                result = self.run_function(app_function, event, context)
5✔
514
                logger.debug("Result of %s:" % whole_function)
5✔
515
                logger.debug(result)
5✔
516
            else:
517
                logger.error("Cannot find a function to process the triggered event.")
×
518
            return result
5✔
519

520
        # This is an HTTP-protocol API Gateway event or Lambda url event with payload format version 2.0
521
        elif "version" in event and event["version"] == "2.0":
5✔
522
            try:
5✔
523
                time_start = datetime.datetime.now()
5✔
524

525
                script_name = ""
5✔
526
                host = event.get("headers", {}).get("host")
5✔
527
                if host:
5✔
528
                    if "amazonaws.com" in host:
5✔
529
                        logger.debug("amazonaws found in host")
5✔
530
                        # The path provided in th event doesn't include the
531
                        # stage, so we must tell Flask to include the API
532
                        # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014
533
                        script_name = f"/{settings.API_STAGE}"
5✔
534
                else:
535
                    # This is a test request sent from the AWS console
NEW
536
                    if settings.DOMAIN:
×
537
                        # Assume the requests received will be on the specified
538
                        # domain. No special handling is required
NEW
539
                        pass
×
540
                    else:
541
                        # Assume the requests received will be to the
542
                        # amazonaws.com endpoint, so tell Flask to include the
543
                        # API stage
NEW
544
                        script_name = f"/{settings.API_STAGE}"
×
545

546
                base_path = getattr(settings, "BASE_PATH", None)
5✔
547
                environ = create_wsgi_request(
5✔
548
                    event,
549
                    script_name=script_name,
550
                    base_path=base_path,
551
                    trailing_slash=self.trailing_slash,
552
                    binary_support=settings.BINARY_SUPPORT,
553
                    context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS,
554
                )
555

556
                # We are always on https on Lambda, so tell our wsgi app that.
557
                environ["HTTPS"] = "on"
5✔
558
                environ["wsgi.url_scheme"] = "https"
5✔
559
                environ["lambda.context"] = context
5✔
560
                environ["lambda.event"] = event
5✔
561

562
                # Execute the application
563
                with Response.from_app(self.wsgi_app, environ) as response:
5✔
564
                    response_body = None
5✔
565
                    response_is_base_64_encoded = False
5✔
566
                    if response.data:
5✔
567
                        response_body, response_is_base_64_encoded = self._process_response_body(response, settings=settings)
5✔
568

569
                    response_status_code = response.status_code
5✔
570

571
                    cookies = []
5✔
572
                    response_headers = {}
5✔
573
                    for key, value in response.headers:
5✔
574
                        if key.lower() == "set-cookie":
5✔
NEW
575
                            cookies.append(value)
×
576
                        else:
577
                            if key in response_headers:
5✔
NEW
578
                                updated_value = f"{response_headers[key]},{value}"
×
NEW
579
                                response_headers[key] = updated_value
×
580
                            else:
581
                                response_headers[key] = value
5✔
582

583
                    # Calculate the total response time,
584
                    # and log it in the Common Log format.
585
                    time_end = datetime.datetime.now()
5✔
586
                    delta = time_end - time_start
5✔
587
                    response_time_ms = delta.total_seconds() * 1000
5✔
588
                    response.content = response.data
5✔
589
                    common_log(environ, response, response_time=response_time_ms)
5✔
590

591
                    return {
5✔
592
                        "cookies": cookies,
593
                        "isBase64Encoded": response_is_base_64_encoded,
594
                        "statusCode": response_status_code,
595
                        "headers": response_headers,
596
                        "body": response_body,
597
                    }
NEW
598
            except Exception as e:
×
599
                # Print statements are visible in the logs either way
NEW
600
                print(e)
×
NEW
601
                exc_info = sys.exc_info()
×
NEW
602
                message = (
×
603
                    "An uncaught exception happened while servicing this request. "
604
                    "You can investigate this with the `zappa tail` command."
605
                )
606

607
                # If we didn't even build an app_module, just raise.
NEW
608
                if not settings.DJANGO_SETTINGS:
×
NEW
609
                    try:
×
NEW
610
                        self.app_module
×
NEW
611
                    except NameError as ne:
×
NEW
612
                        message = "Failed to import module: {}".format(ne.message)
×
613

614
                # Call exception handler for unhandled exceptions
NEW
615
                exception_handler = self.settings.EXCEPTION_HANDLER
×
NEW
616
                self._process_exception(
×
617
                    exception_handler=exception_handler,
618
                    event=event,
619
                    context=context,
620
                    exception=e,
621
                )
622

623
                # Return this unspecified exception as a 500, using template that API Gateway expects.
NEW
624
                content = collections.OrderedDict()
×
NEW
625
                content["statusCode"] = 500
×
NEW
626
                body = {"message": message}
×
NEW
627
                if settings.DEBUG:  # only include traceback if debug is on.
×
NEW
628
                    body["traceback"] = traceback.format_exception(*exc_info)  # traceback as a list for readability.
×
NEW
629
                content["body"] = json.dumps(str(body), sort_keys=True, indent=4)
×
NEW
630
                return content
×
631

632
        # Normal web app flow
633
        try:
5✔
634
            # Timing
635
            time_start = datetime.datetime.now()
5✔
636

637
            # This is a normal HTTP request
638
            if event.get("httpMethod", None):
5✔
639
                script_name = ""
5✔
640
                is_elb_context = False
5✔
641
                headers = merge_headers(event)
5✔
642
                if event.get("requestContext", None) and event["requestContext"].get("elb", None):
5✔
643
                    # Related: https://github.com/Miserlou/Zappa/issues/1715
644
                    # inputs/outputs for lambda loadbalancer
645
                    # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html
646
                    is_elb_context = True
5✔
647
                    # host is lower-case when forwarded from ELB
648
                    host = headers.get("host")
5✔
649
                    # TODO: pathParameters is a first-class citizen in apigateway but not available without
650
                    # some parsing work for ELB (is this parameter used for anything?)
651
                    event["pathParameters"] = ""
5✔
652
                else:
653
                    if headers:
5✔
654
                        host = headers.get("Host")
5✔
655
                    else:
656
                        host = None
5✔
657
                    logger.debug("host found: [{}]".format(host))
5✔
658

659
                    if host:
5✔
660
                        if "amazonaws.com" in host:
5✔
661
                            logger.debug("amazonaws found in host")
5✔
662
                            # The path provided in th event doesn't include the
663
                            # stage, so we must tell Flask to include the API
664
                            # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014
665
                            script_name = "/" + settings.API_STAGE
5✔
666
                    else:
667
                        # This is a test request sent from the AWS console
668
                        if settings.DOMAIN:
5✔
669
                            # Assume the requests received will be on the specified
670
                            # domain. No special handling is required
671
                            pass
5✔
672
                        else:
673
                            # Assume the requests received will be to the
674
                            # amazonaws.com endpoint, so tell Flask to include the
675
                            # API stage
676
                            script_name = "/" + settings.API_STAGE
×
677

678
                base_path = getattr(settings, "BASE_PATH", None)
5✔
679

680
                # Create the environment for WSGI and handle the request
681
                environ = create_wsgi_request(
5✔
682
                    event,
683
                    script_name=script_name,
684
                    base_path=base_path,
685
                    trailing_slash=self.trailing_slash,
686
                    binary_support=settings.BINARY_SUPPORT,
687
                    context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS,
688
                )
689

690
                # We are always on https on Lambda, so tell our wsgi app that.
691
                environ["HTTPS"] = "on"
5✔
692
                environ["wsgi.url_scheme"] = "https"
5✔
693
                environ["lambda.context"] = context
5✔
694
                environ["lambda.event"] = event
5✔
695

696
                # Execute the application
697
                with Response.from_app(self.wsgi_app, environ) as response:
5✔
698
                    # This is the object we're going to return.
699
                    # Pack the WSGI response into our special dictionary.
700
                    zappa_returndict = dict()
5✔
701

702
                    # Issue #1715: ALB support. ALB responses must always include
703
                    # base64 encoding and status description
704
                    if is_elb_context:
5✔
705
                        zappa_returndict.setdefault("isBase64Encoded", False)
5✔
706
                        zappa_returndict.setdefault("statusDescription", response.status)
5✔
707

708
                    if response.data:
5✔
709
                        processed_body, is_base64_encoded = self._process_response_body(response, settings=settings)
5✔
710
                        zappa_returndict["body"] = processed_body
5✔
711
                        if is_base64_encoded:
5✔
712
                            zappa_returndict["isBase64Encoded"] = is_base64_encoded
5✔
713

714
                    zappa_returndict["statusCode"] = response.status_code
5✔
715
                    if "headers" in event:
5✔
716
                        zappa_returndict["headers"] = {}
5✔
717
                        for key, value in response.headers:
5✔
718
                            zappa_returndict["headers"][key] = value
5✔
719
                    if "multiValueHeaders" in event:
5✔
720
                        zappa_returndict["multiValueHeaders"] = {}
5✔
721
                        for key, value in response.headers:
5✔
722
                            zappa_returndict["multiValueHeaders"][key] = response.headers.getlist(key)
5✔
723

724
                    # Calculate the total response time,
725
                    # and log it in the Common Log format.
726
                    time_end = datetime.datetime.now()
5✔
727
                    delta = time_end - time_start
5✔
728
                    response_time_us = delta.total_seconds() * 1_000_000  # convert to microseconds
5✔
729
                    response.content = response.data
5✔
730
                    common_log(environ, response, response_time=response_time_us)
5✔
731

732
                    return zappa_returndict
5✔
733
        except Exception as e:  # pragma: no cover
734
            # Print statements are visible in the logs either way
735
            print(e)
736
            exc_info = sys.exc_info()
737
            message = (
738
                "An uncaught exception happened while servicing this request. "
739
                "You can investigate this with the `zappa tail` command."
740
            )
741

742
            # If we didn't even build an app_module, just raise.
743
            if not settings.DJANGO_SETTINGS:
744
                try:
745
                    self.app_module
746
                except NameError as ne:
747
                    message = "Failed to import module: {}".format(ne.message)
748

749
            # Call exception handler for unhandled exceptions
750
            exception_handler = self.settings.EXCEPTION_HANDLER
751
            self._process_exception(
752
                exception_handler=exception_handler,
753
                event=event,
754
                context=context,
755
                exception=e,
756
            )
757

758
            # Return this unspecified exception as a 500, using template that API Gateway expects.
759
            content = collections.OrderedDict()
760
            content["statusCode"] = 500
761
            body = {"message": message}
762
            if settings.DEBUG:  # only include traceback if debug is on.
763
                body["traceback"] = traceback.format_exception(*exc_info)  # traceback as a list for readability.
764
            content["body"] = json.dumps(str(body), sort_keys=True, indent=4)
765
            return content
766

767

768
def lambda_handler(event, context):  # pragma: no cover
769
    return LambdaHandler.lambda_handler(event, context)
770

771

772
def keep_warm_callback(event, context):
5✔
773
    """Method is triggered by the CloudWatch event scheduled when keep_warm setting is set to true."""
774
    lambda_handler(event={}, context=context)  # overriding event with an empty one so that web app initialization will
×
775
    # be triggered.
776

777

778
global_handler = None
5✔
779
if os.environ.get("INSTANTIATE_LAMBDA_HANDLER_ON_IMPORT"):
5✔
780
    global_handler = LambdaHandler()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc