Skip to content

bug: API Gateway, with type=AWS does not match behaviour we see in AWS. #12574

Closed
@plattr30

Description

@plattr30

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

API Gateway, with type=AWS does not match behaviour we see in AWS.

API Gateway is matching ".+" with the 200 response. It looks like it is returning "{}" in the errorMessage rather than an empty string.

curl $(terraform output -raw hello_url)
{
"errorMessage": "{}",
"errorType": ""
}

From AWS doc page https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-responses.html

For Lambda invocations, the regex applies to the errorMessage field of the error information object returned by AWS Lambda as a failure response body when the Lambda function execution throws an exception.

In AWS if an exception is not raised the errorMessage is an empty string so regex ".+" will not match but in Localstack the errorMessage is "{}" so regex ".+" matches

lambda_function.py:

`import json

def lambda_handler(event, context):
path = event.get("path", "")

if path.endswith("/error"):
    # Return a structured error message to API Gateway
    raise Exception("Boom! This is a real error")

return {
    "statusCode": 200,
    "body": json.dumps({"message": "All good!"})
}

`

main.tf:
`provider "aws" {
access_key = "test"
secret_key = "test"
region = "us-east-1"
s3_use_path_style = false
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true

endpoints {
apigateway = "http://localhost:4566"
lambda = "http://localhost:4566"
iam = "http://localhost:4566"
}
}

data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda_function.py"
output_path = "${path.module}/lambda_function_payload.zip"
}

resource "aws_lambda_function" "hello_lambda" {
filename = data.archive_file.lambda_zip.output_path
function_name = "hello-lambda"
role = "arn:aws:iam::000000000000:role/lambda-role"
handler = "lambda_function.lambda_handler"
runtime = "python3.9"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
}

resource "aws_api_gateway_rest_api" "example" {
name = "example-api"
}

resource "aws_api_gateway_resource" "hello" {
rest_api_id = aws_api_gateway_rest_api.example.id
parent_id = aws_api_gateway_rest_api.example.root_resource_id
path_part = "hello"
}

resource "aws_api_gateway_resource" "error" {
rest_api_id = aws_api_gateway_rest_api.example.id
parent_id = aws_api_gateway_rest_api.example.root_resource_id
path_part = "error"
}

locals {
api_resources = {
hello = aws_api_gateway_resource.hello
error = aws_api_gateway_resource.error
}
}

resource "aws_api_gateway_method" "get_method" {
for_each = local.api_resources
rest_api_id = aws_api_gateway_rest_api.example.id
resource_id = each.value.id
http_method = "GET"
authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda_integration" {
for_each = local.api_resources
rest_api_id = aws_api_gateway_rest_api.example.id
resource_id = each.value.id
http_method = "GET"
integration_http_method = "POST"
type = "AWS"
uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${aws_lambda_function.hello_lambda.arn}/invocations"

request_templates = {
"application/json" = <<EOF
{
"path": "$context.resourcePath"
}
EOF
}

passthrough_behavior = "WHEN_NO_MATCH"
}

resource "aws_api_gateway_method_response" "success" {
for_each = local.api_resources
rest_api_id = aws_api_gateway_rest_api.example.id
resource_id = each.value.id
http_method = "GET"
status_code = "200"

response_models = {
"application/json" = "Empty"
}

depends_on = [
aws_api_gateway_method.get_method
]
}

resource "aws_api_gateway_integration_response" "success" {
for_each = local.api_resources
rest_api_id = aws_api_gateway_rest_api.example.id
resource_id = each.value.id
http_method = "GET"
status_code = "200"
selection_pattern = ""

response_templates = {
"application/json" = "$input.body"
}

depends_on = [
aws_api_gateway_method.get_method,
aws_api_gateway_integration.lambda_integration
]
}

resource "aws_api_gateway_method_response" "error" {
for_each = local.api_resources
rest_api_id = aws_api_gateway_rest_api.example.id
resource_id = each.value.id
http_method = "GET"
status_code = "500"

depends_on = [
aws_api_gateway_method.get_method
]
}

resource "aws_api_gateway_integration_response" "error" {
for_each = local.api_resources
rest_api_id = aws_api_gateway_rest_api.example.id
resource_id = each.value.id
http_method = "GET"
status_code = "500"

selection_pattern = ".+"

response_templates = {
"application/json" = <<EOF
#set($root = $input.path("$"))
{
"errorMessage": "$util.escapeJavaScript($root.errorMessage).replaceAll("\'","'")",
"errorType": "$root.errorType"
}
EOF
}

depends_on = [
aws_api_gateway_method.get_method,
aws_api_gateway_integration.lambda_integration
]
}

resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_lambda.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.example.execution_arn}//"
}

resource "aws_api_gateway_deployment" "deployment" {
rest_api_id = aws_api_gateway_rest_api.example.id

depends_on = [
aws_lambda_function.hello_lambda,
aws_api_gateway_method.get_method,
aws_api_gateway_integration.lambda_integration,
aws_api_gateway_integration_response.success,
aws_api_gateway_integration_response.error,
aws_api_gateway_method_response.success,
aws_api_gateway_method_response.error
]

triggers = {
redeployment = sha1(jsonencode(keys(local.api_resources)))
}

lifecycle {
create_before_destroy = true
}
}

resource "aws_api_gateway_stage" "stage" {
deployment_id = aws_api_gateway_deployment.deployment.id
rest_api_id = aws_api_gateway_rest_api.example.id
stage_name = "example"
}

output "hello_url" {
value = "http://localhost:4566/restapis/${aws_api_gateway_rest_api.example.id}/${aws_api_gateway_stage.stage.stage_name}/_user_request_/hello"
}

output "error_url" {
value = "http://localhost:4566/restapis/${aws_api_gateway_rest_api.example.id}/${aws_api_gateway_stage.stage.stage_name}/_user_request_/error"
}

output "rest_api_id" {
description = "API Gateway REST API ID"
value = aws_api_gateway_rest_api.example.id
}

output "stage_name" {
description = "Deployed stage name"
value = aws_api_gateway_stage.stage.stage_name
}

output "hello_resource_id" {
description = "Resource ID for /hello"
value = aws_api_gateway_resource.hello.id
}

output "error_resource_id" {
description = "Resource ID for /error"
value = aws_api_gateway_resource.error.id
}

`

Expected Behavior

return a 200 response with a GET /hello

How are you starting LocalStack?

docker compose up -d

Steps To Reproduce

As above

Environment

- OS:
- LocalStack:
  LocalStack version: latest
  LocalStack Docker image sha:
  LocalStack build date:
  LocalStack build git hash:

Anything else?

No response

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions