Skip to content

Instantly share code, notes, and snippets.

@nicc777
Created August 11, 2022 04:44
Show Gist options
  • Save nicc777/a2e3472764939fa3a1f1d79eb558fb0f to your computer and use it in GitHub Desktop.
Save nicc777/a2e3472764939fa3a1f1d79eb558fb0f to your computer and use it in GitHub Desktop.
AWS Lambda Function Template for AWS HTTP API Gateway Proxy Requests

The function is for a SAM template that looks something like the following:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Example Template for AWS HTTP API to Python Lambda Function

Parameters:
  StageNameParameter:
    Type: String
    Description: The API Gateway Stage Name
    Default: sandbox

Resources:

  HttpApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: !Ref StageNameParameter
      Tags:
        Tag: Value
      AccessLogSettings:
        DestinationArn: !GetAtt HttpApiAccessLogs.Arn
        Format: $context.stage $context.integrationErrorMessage $context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.responseLength $context.requestId $context.extendedRequestId
      DefaultRouteSettings:
        ThrottlingBurstLimit: 200
      RouteSettings:
        "POST /example":
          ThrottlingBurstLimit: 500 # overridden in HttpApi Event
      StageVariables:
        StageVar: Value
      FailOnWarnings: true

  HttpApiAccessLogs:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 90

  ApiFunctionLogs:
    Type: AWS::Logs::LogGroup
    Properties:
        LogGroupName: !Sub /aws/lambda/${ApiFunction}
        RetentionInDays: 7

  ApiFunction: # Adds a GET api endpoint at "/" to the ApiGatewayApi via an Api event
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ExplicitApi: # warning: creates a public endpoint
          Type: HttpApi
          Properties:
            ApiId: !Ref HttpApi
            Method: POST
            Path: /example
            TimeoutInMillis: 30000
            PayloadFormatVersion: "2.0"
            RouteSettings:
              ThrottlingBurstLimit: 600
      Runtime: python3.8
      Handler: function.handler
      CodeUri: path/to/src

One of two requests can now be made that will both be parsed correctly: first with JSON payload and the second with FORM DATA:

curl -d '{"Message": "Test123"}' -H "Content-Type: application/json" -X POST https://nnnnnnnnnn.execute-api.eu-central-1.amazonaws.com/sandbox/example

curl -d "param1=value1&param2=value2" -X POST  https://nnnnnnnnnn.execute-api.eu-central-1.amazonaws.com/sandbox/example
import boto3
import traceback
import os
import json
import logging
from datetime import datetime
import sys
import base64
from urllib.parse import parse_qs
def extract_post_data(event)->str:
if 'requestContext' in event:
if 'http' in event['requestContext']:
if 'method' in event['requestContext']['http']:
if event['requestContext']['http']['method'].upper() in ('POST', 'PUT', 'DELETE'): # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
if 'isBase64Encoded' in event and 'body' in event:
if event['isBase64Encoded'] is True:
body = base64.b64decode(event['body'])
if isinstance(body, bytes):
body = body.decode('utf-8')
return body
if 'body' in event:
body = event['body']
if isinstance(body, bytes):
body = body.decode('utf-8')
else:
body = '{}'.format(body)
return body
return ""
def decode_data(event, body: str):
if 'headers' in event:
if 'content-type' in event['headers']:
if 'json' in event['headers']['content-type'].lower():
return json.loads(body)
if 'x-www-form-urlencoded' in event['headers']['content-type'].lower():
return parse_qs(body)
return body
def get_logger(level=logging.INFO):
logger = logging.getLogger()
for h in logger.handlers:
logger.removeHandler(h)
formatter = logging.Formatter('%(funcName)s:%(lineno)d - %(levelname)s - %(message)s')
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(level)
ch.setFormatter(formatter)
logger.addHandler(ch)
logger.setLevel(level)
return logger
def get_client(client_name: str, region: str='eu-central-1', boto3_clazz=boto3):
return boto3_clazz.client(client_name, region_name=region)
CACHE_TTL_DEFAULT = 600
cache = dict()
def get_utc_timestamp(with_decimal: bool = False):
epoch = datetime(1970, 1, 1, 0, 0, 0)
now = datetime.utcnow()
timestamp = (now - epoch).total_seconds()
if with_decimal:
return timestamp
return int(timestamp)
def get_debug()->bool:
try:
return bool(int(os.getenv('DEBUG', '0')))
except:
pass
return False
def get_cache_ttl(logger=get_logger())->int:
try:
return int(os.getenv('CACHE_TTL', '{}'.format(CACHE_TTL_DEFAULT)))
except:
logger.error('EXCEPTION: {}'.format(traceback.format_exc()))
return CACHE_TTL_DEFAULT
def refresh_environment_cache(logger=get_logger()):
global cache
now = get_utc_timestamp(with_decimal=False)
if 'Environment' in cache:
if cache['Environment']['Expiry'] > now:
return
cache['Environment'] = {
'Expiry': get_utc_timestamp() + get_cache_ttl(logger=logger),
'Data': {
'CACHE_TTL': get_cache_ttl(logger=logger),
'DEBUG': get_debug(),
# Other ENVIRONMENT variables can be added here... The environment will be re-read after the CACHE_TTL
}
}
logger.debug('cache: {}'.format((json.dumps(cache))))
###############################################################################
### ###
### M A I N H A N D L E R ###
### ###
###############################################################################
def handler(
event,
context,
logger=get_logger(level=logging.INFO),
boto3_clazz=boto3,
run_from_main: bool=False
):
result = dict()
return_object = {
'statusCode': 200,
'headers': {
'x-custom-header' : 'my custom header value',
'content-type': 'application/json',
},
'body': result,
'isBase64Encoded': False,
}
refresh_environment_cache(logger=logger)
if cache['Environment']['Data']['DEBUG'] is True and run_from_main is False:
logger = get_logger(level=logging.DEBUG)
logger.info('HANDLER CALLED')
logger.debug('DEBUG ENABLED')
logger.info('event={}'.format(event))
body = extract_post_data(event=event)
logger.info('body={}'.format(body))
data = decode_data(event=event, body=body)
logger.info('data={}'.format(data))
result['message'] = 'ok'
return_object['body'] = json.dumps(result)
logger.info('HANDLER DONE')
logger.info('result={}'.format(result))
logger.info('return_object={}'.format(return_object))
return return_object
###############################################################################
### ###
### M A I N F U N C T I O N ###
### ###
###############################################################################
if __name__ == '__main__':
logger = logging.getLogger("my_lambda")
formatter = logging.Formatter('%(asctime)s - %(name)s - %(funcName)s:%(lineno)d - %(levelname)s - %(message)s')
ch = logging.StreamHandler()
if get_debug() is True:
ch.setLevel(logging.DEBUG)
else:
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)
if get_debug() is True:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
handler(event={}, context=None, logger=logger, run_from_main=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment