hello
-
-
Save jhw/3aa9d4f85d9709fc7cb44e886ba0808f to your computer and use it in GitHub Desktop.
env | |
*.pyc | |
__pycache__ | |
tmp | |
setenv-priv.sh |
AppName=apigw-post-model-demo |
#!/usr/bin/env bash | |
. app.props | |
aws cloudformation delete-stack --stack-name $AppName |
#!/usr/bin/env bash | |
. app.props | |
echo "DomainName: $DOMAIN_NAME" | |
echo "CertificateArn: $CERTIFICATE_ARN" | |
aws cloudformation deploy --stack-name $AppName --template-file stack.json --capabilities CAPABILITY_NAMED_IAM --parameter-overrides DomainName=$DOMAIN_NAME CertificateArn=$CERTIFICATE_ARN | |
I have a Cloudformation template for an apigw HTTP POST endpoint, which validates the POST request body using a Model -
{
"Outputs": {
"AppRestApi": {
"Value": {
"Ref": "AppRestApi"
}
}
},
"Parameters": {
"CertificateArn": {
"Type": "String"
},
"DomainName": {
"Type": "String"
}
},
"Resources": {
"AppBasePathMapping": {
"DependsOn": [
"AppDomainName"
],
"Properties": {
"DomainName": {
"Ref": "DomainName"
},
"RestApiId": {
"Ref": "AppRestApi"
},
"Stage": "prod"
},
"Type": "AWS::ApiGateway::BasePathMapping"
},
"AppDeployment": {
"DependsOn": [
"AppHelloPostPublicLambdaProxyMethod"
],
"Properties": {
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Deployment"
},
"AppDomainName": {
"Properties": {
"CertificateArn": {
"Ref": "CertificateArn"
},
"DomainName": {
"Ref": "DomainName"
}
},
"Type": "AWS::ApiGateway::DomainName"
},
"AppHelloPostFunction": {
"Properties": {
"Code": {
"ZipFile": "import json\ndef handler(event, context):\n body=json.loads(event[\"body\"])\n message=body[\"message\"]\n return {\"statusCode\": 200,\n \"headers\": {\"Content-Type\": \"text/plain\",\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Headers\": \"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent\",\n \"Access-Control-Allow-Methods\": \"OPTIONS,POST\"},\n \"body\": f\"you sent '{message}' via POST\"}"
},
"Handler": "index.handler",
"MemorySize": 512,
"Role": {
"Fn::GetAtt": [
"AppHelloPostRole",
"Arn"
]
},
"Runtime": "python3.10",
"Timeout": 5
},
"Type": "AWS::Lambda::Function"
},
"AppHelloPostModel": {
"Properties": {
"ContentType": "application/json",
"Name": "AppHelloPostModel",
"RestApiId": {
"Ref": "AppRestApi"
},
"Schema": {
"$schema": "http://json-schema.org/draft-04/schema#",
"additionalProperties": false,
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
}
},
"Type": "AWS::ApiGateway::Model"
},
"AppHelloPostPermission": {
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "AppHelloPostFunction"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppRestApi}/${AppStage}/POST/hello-post"
}
},
"Type": "AWS::Lambda::Permission"
},
"AppHelloPostPublicLambdaProxyMethod": {
"Properties": {
"AuthorizationType": "NONE",
"HttpMethod": "POST",
"Integration": {
"IntegrationHttpMethod": "POST",
"Type": "AWS_PROXY",
"Uri": {
"Fn::Sub": [
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations",
{
"arn": {
"Fn::GetAtt": [
"AppHelloPostFunction",
"Arn"
]
}
}
]
}
},
"RequestModels": {
"application/json": "AppHelloPostModel"
},
"RequestValidatorId": {
"Ref": "AppHelloPostSchemaRequestValidator"
},
"ResourceId": {
"Ref": "AppHelloPostResource"
},
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Method"
},
"AppHelloPostResource": {
"Properties": {
"ParentId": {
"Fn::GetAtt": [
"AppRestApi",
"RootResourceId"
]
},
"PathPart": "hello-post",
"RestApiId": {
"Ref": "AppRestApi"
}
},
"Type": "AWS::ApiGateway::Resource"
},
"AppHelloPostRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": [
"sts:AssumeRole"
],
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"Policies": [
{
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": {
"Fn::Sub": "app-hello-post-role-policy-${AWS::StackName}"
}
}
]
},
"Type": "AWS::IAM::Role"
},
"AppHelloPostSchemaRequestValidator": {
"Properties": {
"RestApiId": {
"Ref": "AppRestApi"
},
"ValidateRequestBody": true,
"ValidateRequestParameters": false
},
"Type": "AWS::ApiGateway::RequestValidator"
},
"AppRecordSet": {
"Properties": {
"AliasTarget": {
"DNSName": {
"Fn::GetAtt": [
"AppDomainName",
"DistributionDomainName"
]
},
"EvaluateTargetHealth": false,
"HostedZoneId": {
"Fn::GetAtt": [
"AppDomainName",
"DistributionHostedZoneId"
]
}
},
"HostedZoneName": {
"Fn::Sub": [
"${prefix}.${suffix}.",
{
"prefix": {
"Fn::Select": [
1,
{
"Fn::Split": [
".",
{
"Ref": "DomainName"
}
]
}
]
},
"suffix": {
"Fn::Select": [
2,
{
"Fn::Split": [
".",
{
"Ref": "DomainName"
}
]
}
]
}
}
]
},
"Name": {
"Ref": "DomainName"
},
"Type": "A"
},
"Type": "AWS::Route53::RecordSet"
},
"AppRestApi": {
"Properties": {
"Name": {
"Fn::Sub": "app-rest-api-${AWS::StackName}"
}
},
"Type": "AWS::ApiGateway::RestApi"
},
"AppStage": {
"Properties": {
"DeploymentId": {
"Ref": "AppDeployment"
},
"RestApiId": {
"Ref": "AppRestApi"
},
"StageName": "prod"
},
"Type": "AWS::ApiGateway::Stage"
}
}
}
This works fine in the happy path case -
(env) jhw@Justins-Air % curl -i -X POST https://apigwmodelpostdemo.spaas.link/hello-post -d "{\"message\": \"Hello World\!\"}"
HTTP/2 200
content-type: text/plain
content-length: 32
date: Fri, 15 Mar 2024 06:14:57 GMT
x-amzn-requestid: 7f7ad199-6bb7-4ea9-bcab-9046d5c8651e
access-control-allow-origin: *
access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
x-amz-apigw-id: UqEXOEnZjoEEYmA=
access-control-allow-methods: OPTIONS,POST
x-amzn-trace-id: Root=1-65f3e761-2bf936e212c5205716168100;Parent=3218d4aa6fbde101;Sampled=0;lineage=3282c475:0
x-cache: Miss from cloudfront
via: 1.1 22e421a47e59010b5e8eb6ae4d4bd7e4.cloudfront.net (CloudFront)
x-amz-cf-pop: LHR61-P2
x-amz-cf-id: -fpNsR5L4xGvFf-GwO-aDw4D8TkfA1cc0B6fnoLLFDZC0gP_2vcFLQ==
you sent 'Hello World!' via POST%
but when I send it with what should be an invalid request body it returns HTTP 5XX not HTTP 4XX -
(env) jhw@Justins-Air % curl -i -X POST https://apigwmodelpostdemo.spaas.link/hello-post -d "{\"_message\": \"Hello World\!\"}"
HTTP/2 502
content-type: application/json
content-length: 36
date: Fri, 15 Mar 2024 06:15:02 GMT
x-amzn-requestid: 9113503e-79c7-4948-bb3d-9cb3698d2ff9
x-amzn-errortype: InternalServerErrorException
x-amz-apigw-id: UqEYCFd2joEED1Q=
x-cache: Error from cloudfront
via: 1.1 8ba281782b2b20f7db8f5372bc06a3a2.cloudfront.net (CloudFront)
x-amz-cf-pop: LHR61-P2
x-amz-cf-id: UpUa11Il2x7lEdu7l7SboDyuB9TekNAsnY_fo8INHX7LDlgal81nDw==
{"message": "Internal server error"}%
This error is coming from the Lambda function, as I can see the error in the Lambda logs. Which suggests that the apigw Model is not being called, or if it is being called, it is not configured to return a 4XX response.
Why is that and what needs to be changed, so that if the endpoint is called with an invalid request body, apigw will return 4XX because of Model validation failure, and the request will never hit the Lambda function?
I have tested that the schema is formatted properly -
import json, jsonschema
if __name__ == "__main__":
template=json.loads(open("template.json").read())
schema=template["Resources"]["AppHelloPostModel"]["Properties"]["Schema"]
for instance in [{"message": "hello"},
{"_message": "hello"}]:
print(f"--- {instance} ---")
try:
jsonschema.validate(instance=instance,
schema=schema)
print("OK")
except jsonschema.exceptions.ValidationError as error:
print(str(error))
env) jhw@Justins-Air % python test_model_validation.py
--- {'message': 'hello'} ---
OK
--- {'_message': 'hello'} ---
Additional properties are not allowed ('_message' was unexpected)
Failed validating 'additionalProperties' in schema:
{'$schema': 'http://json-schema.org/draft-04/schema#',
'additionalProperties': False,
'properties': {'message': {'type': 'string'}},
'required': ['message'],
'type': 'object'}
On instance:
{'_message': 'hello'}
Please let me know what I need to change, to make sure the Model is validating the incoming request body properly.
#!/usr/bin/env bash | |
. app.props | |
aws cloudformation describe-stack-events --stack-name $AppName --query "StackEvents[].{\"1.Timestamp\":Timestamp,\"2.Id\":LogicalResourceId,\"3.Type\":ResourceType,\"4.Status\":ResourceStatus,\"5.Reason\":ResourceStatusReason}" |
#!/usr/bin/env bash | |
. app.props | |
aws cloudformation describe-stacks --stack-name $AppName --query 'Stacks[0].Outputs' --output table |
#!/usr/bin/env bash | |
. app.props | |
aws cloudformation describe-stack-resources --stack-name $AppName --query "StackResources[].{\"1.Timestamp\":Timestamp,\"2.LogicalId\":LogicalResourceId,\"3.PhysicalId\":PhysicalResourceId,\"4.Type\":ResourceType,\"5.Status\":ResourceStatus}" |
#!/usr/bin/env bash | |
aws cloudformation describe-stacks --query "Stacks[].{\"1.Name\":StackName,\"2.Status\":StackStatus}" |
awscli | |
boto3 | |
botocore | |
jsonschema | |
pyyaml |
#!/usr/bin/env bash | |
export AWS_DEFAULT_OUTPUT=table | |
export AWS_PROFILE=#{your-aws-profile-here} | |
export CERTIFICATE_ARN=#{your-certificate-arn-here} | |
export DOMAIN_NAME=#{your-fully-qualified-domain-name-here} | |
{ | |
"Outputs": { | |
"AppRestApi": { | |
"Value": { | |
"Ref": "AppRestApi" | |
} | |
} | |
}, | |
"Parameters": { | |
"CertificateArn": { | |
"Type": "String" | |
}, | |
"DomainName": { | |
"Type": "String" | |
} | |
}, | |
"Resources": { | |
"AppBasePathMapping": { | |
"DependsOn": [ | |
"AppDomainName" | |
], | |
"Properties": { | |
"DomainName": { | |
"Ref": "DomainName" | |
}, | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
}, | |
"Stage": "prod" | |
}, | |
"Type": "AWS::ApiGateway::BasePathMapping" | |
}, | |
"AppDeployment": { | |
"DependsOn": [ | |
"AppHelloPostPublicLambdaProxyMethod" | |
], | |
"Properties": { | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
} | |
}, | |
"Type": "AWS::ApiGateway::Deployment" | |
}, | |
"AppDomainName": { | |
"Properties": { | |
"CertificateArn": { | |
"Ref": "CertificateArn" | |
}, | |
"DomainName": { | |
"Ref": "DomainName" | |
} | |
}, | |
"Type": "AWS::ApiGateway::DomainName" | |
}, | |
"AppHelloPostFunction": { | |
"Properties": { | |
"Code": { | |
"ZipFile": "import json\ndef handler(event, context):\n body=json.loads(event[\"body\"])\n message=body[\"message\"]\n return {\"statusCode\": 200,\n \"headers\": {\"Content-Type\": \"text/plain\",\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Headers\": \"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent\",\n \"Access-Control-Allow-Methods\": \"OPTIONS,POST\"},\n \"body\": f\"you sent '{message}' via POST\"}" | |
}, | |
"Handler": "index.handler", | |
"MemorySize": 512, | |
"Role": { | |
"Fn::GetAtt": [ | |
"AppHelloPostRole", | |
"Arn" | |
] | |
}, | |
"Runtime": "python3.10", | |
"Timeout": 5 | |
}, | |
"Type": "AWS::Lambda::Function" | |
}, | |
"AppHelloPostModel": { | |
"Properties": { | |
"ContentType": "application/json", | |
"Name": "AppHelloPostModel", | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
}, | |
"Schema": { | |
"$schema": "http://json-schema.org/draft-04/schema#", | |
"additionalProperties": false, | |
"properties": { | |
"message": { | |
"type": "string" | |
} | |
}, | |
"required": [ | |
"message" | |
], | |
"type": "object" | |
} | |
}, | |
"Type": "AWS::ApiGateway::Model" | |
}, | |
"AppHelloPostPermission": { | |
"Properties": { | |
"Action": "lambda:InvokeFunction", | |
"FunctionName": { | |
"Ref": "AppHelloPostFunction" | |
}, | |
"Principal": "apigateway.amazonaws.com", | |
"SourceArn": { | |
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${AppRestApi}/${AppStage}/POST/hello-post" | |
} | |
}, | |
"Type": "AWS::Lambda::Permission" | |
}, | |
"AppHelloPostPublicLambdaProxyMethod": { | |
"Properties": { | |
"AuthorizationType": "NONE", | |
"HttpMethod": "POST", | |
"Integration": { | |
"IntegrationHttpMethod": "POST", | |
"Type": "AWS_PROXY", | |
"Uri": { | |
"Fn::Sub": [ | |
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${arn}/invocations", | |
{ | |
"arn": { | |
"Fn::GetAtt": [ | |
"AppHelloPostFunction", | |
"Arn" | |
] | |
} | |
} | |
] | |
} | |
}, | |
"RequestModels": { | |
"application/json": "AppHelloPostModel" | |
}, | |
"RequestValidatorId": { | |
"Ref": "AppHelloPostSchemaRequestValidator" | |
}, | |
"ResourceId": { | |
"Ref": "AppHelloPostResource" | |
}, | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
} | |
}, | |
"Type": "AWS::ApiGateway::Method" | |
}, | |
"AppHelloPostResource": { | |
"Properties": { | |
"ParentId": { | |
"Fn::GetAtt": [ | |
"AppRestApi", | |
"RootResourceId" | |
] | |
}, | |
"PathPart": "hello-post", | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
} | |
}, | |
"Type": "AWS::ApiGateway::Resource" | |
}, | |
"AppHelloPostRole": { | |
"Properties": { | |
"AssumeRolePolicyDocument": { | |
"Statement": [ | |
{ | |
"Action": [ | |
"sts:AssumeRole" | |
], | |
"Effect": "Allow", | |
"Principal": { | |
"Service": "lambda.amazonaws.com" | |
} | |
} | |
], | |
"Version": "2012-10-17" | |
}, | |
"Policies": [ | |
{ | |
"PolicyDocument": { | |
"Statement": [ | |
{ | |
"Action": [ | |
"logs:CreateLogGroup", | |
"logs:CreateLogStream", | |
"logs:PutLogEvents" | |
], | |
"Effect": "Allow", | |
"Resource": "*" | |
} | |
], | |
"Version": "2012-10-17" | |
}, | |
"PolicyName": { | |
"Fn::Sub": "app-hello-post-role-policy-${AWS::StackName}" | |
} | |
} | |
] | |
}, | |
"Type": "AWS::IAM::Role" | |
}, | |
"AppHelloPostSchemaRequestValidator": { | |
"Properties": { | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
}, | |
"ValidateRequestBody": true, | |
"ValidateRequestParameters": false | |
}, | |
"Type": "AWS::ApiGateway::RequestValidator" | |
}, | |
"AppRecordSet": { | |
"Properties": { | |
"AliasTarget": { | |
"DNSName": { | |
"Fn::GetAtt": [ | |
"AppDomainName", | |
"DistributionDomainName" | |
] | |
}, | |
"EvaluateTargetHealth": false, | |
"HostedZoneId": { | |
"Fn::GetAtt": [ | |
"AppDomainName", | |
"DistributionHostedZoneId" | |
] | |
} | |
}, | |
"HostedZoneName": { | |
"Fn::Sub": [ | |
"${prefix}.${suffix}.", | |
{ | |
"prefix": { | |
"Fn::Select": [ | |
1, | |
{ | |
"Fn::Split": [ | |
".", | |
{ | |
"Ref": "DomainName" | |
} | |
] | |
} | |
] | |
}, | |
"suffix": { | |
"Fn::Select": [ | |
2, | |
{ | |
"Fn::Split": [ | |
".", | |
{ | |
"Ref": "DomainName" | |
} | |
] | |
} | |
] | |
} | |
} | |
] | |
}, | |
"Name": { | |
"Ref": "DomainName" | |
}, | |
"Type": "A" | |
}, | |
"Type": "AWS::Route53::RecordSet" | |
}, | |
"AppRestApi": { | |
"Properties": { | |
"Name": { | |
"Fn::Sub": "app-rest-api-${AWS::StackName}" | |
} | |
}, | |
"Type": "AWS::ApiGateway::RestApi" | |
}, | |
"AppStage": { | |
"Properties": { | |
"DeploymentId": { | |
"Ref": "AppDeployment" | |
}, | |
"RestApiId": { | |
"Ref": "AppRestApi" | |
}, | |
"StageName": "prod" | |
}, | |
"Type": "AWS::ApiGateway::Stage" | |
} | |
} | |
} |
import json, jsonschema | |
if __name__ == "__main__": | |
template=json.loads(open("stack.json").read()) | |
schema=template["Resources"]["AppHelloPostModel"]["Properties"]["Schema"] | |
for instance in [{"message": "hello"}, | |
{"_message": "hello"}]: | |
print(f"--- {instance} ---") | |
try: | |
jsonschema.validate(instance=instance, | |
schema=schema) | |
print("OK") | |
except jsonschema.exceptions.ValidationError as error: | |
print(str(error)) | |