Tracking down the Cause of Internal Server Error with AWS HTTP API Gateway

Tracking down the Cause of Internal Server Error with AWS HTTP API Gateway

📅 December 21, 2020 – Kyle Galbraith

Recently I have been migrating the API of one of my original side projects, Ultimate Fantasy Supercross, to be 100% serverless. The API has moved over to .NET Core 3.1 and now it's down to porting each endpoint into Lambda functions.

To ease the deployment pain I chose to make use of the dotnet Lambda tools provided by AWS. The deploy-severless functionality allows me to represent a collection of endpoints as CloudFormation in one project inside of a serverless.template file. Then to deploy it, I run dotnet lambda deploy-serverless and the entire application gets deployed.

My first endpoint was a simple GET endpoint. It had a custom authorizer on it that integrates with my Auth0. The custom authorizer function was already deployed and deemed working. My endpoint inside my serverless.template looked like this:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "An AWS Serverless Application.",
  "Resources": {
    "LeaguesApi": {
      "Type": "AWS::Serverless::HttpApi",
      "Properties": {
        "AccessLogSettings": {
          "DestinationArn": "arn:aws:logs:us-west-2:249704159252:log-group:http-api-gateway-access-logs",
          "Format": "$context.identity.sourceIp - - [$context.requestTime] context.httpMethod $context.routeKey $context.protocol $context.status $context.responseLength $context.requestId"
        },
        "Auth": {
          "Authorizers": {
            "Auth0LambdaArn": {
              "EnableSimpleResponses": true,
              "AuthorizerPayloadFormatVersion": "2.0",
              "FunctionArn": "arn:aws:lambda:us-west-2:249704159252:function:ufsx-auth0-authorizer-dev-auth"
            }
          }
        },
        "DefaultRouteSettings": {
          "DetailedMetricsEnabled": true
        },
        "StageName": "prod"
      }
    },
    "GetLeagues": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "UFSX.Api.Leagues::UFSX.Api.Leagues.Functions::Get",
        "Runtime": "dotnetcore3.1",
        "CodeUri": "",
        "MemorySize": 1024,
        "Timeout": 30,
        "Role": "arn:aws:iam::<account-id>:role/lambda-database-with-put-event-bus",
        "Policies": ["AWSLambdaFullAccess"],
        "VpcConfig": {
          "SecurityGroupIds": ["sg-12345678"],
          "SubnetIds": ["subnet-8746aa78", "subnet-428agdfg"]
        },
        "Events": {
          "RootGet": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/leagues",
              "Method": "GET",
              "Auth": {
                "Authorizer": "Auth0LambdaArn"
              },
              "ApiId": {
                "Ref": "LeaguesApi"
              }
            }
          }
        }
      }
    }
  }
}

It looks like there is a lot happening here but that's just the verbosity of CloudFormation. What we have here is an HTTP API Gateway defined called LeaguesApi. This is the HTTP API approach provided by AWS which provides a cheaper and more performant alternative to their original REST API. Inside of the API block, we enabled access logging to a CloudWatch log group. We also defined our Custom Authorizer with the ARN of the deployed function.

Further down we define our first function, GetLeagues. It has some VPC configuration for talking to an RDS database as well as an Events block. Inside that block, we define the API path this function listens to. There is also the authorizer that sits in front of it and the HTTP API Gateway it is connected to.

With that template setup and a basic Hello World running inside of the function, I deployed it.

Why do I get an Internal Server Error when I invoke an HTTP API Gateway endpoint?

Shortly after deploying it, I tried hitting my new endpoint with the correct Auth0 token.

{"message": "Internal Server Error"}

That's odd, my Lambda function is just writing to the logs Hello World. So I went to the CloudWatch logs for both the authorizer function and the function supporting my API. But looking at the logs for both Lambda functions revealed that neither one had been invoked 🤔

The troubleshooting docs tell you to enable access logs for your API. Which we did up in the original serverless.template with this line.

"Format": "$context.identity.sourceIp - - [$context.requestTime] context.httpMethod $context.routeKey $context.protocol $context.status $context.responseLength $context.requestId"

But according to the troubleshooting docs, we should add $context.integrationErrorMessage to our access logs format. This allows us to see what/if there is an integration error. An integration error would indicate that our API Gateway endpoint is not able to call our Lambda function. So I updated my access logs format to add the extra variable.

"Format": "$context.identity.sourceIp - - [$context.requestTime] context.httpMethod $context.routeKey $context.protocol $context.status $context.responseLength $context.requestId $context.integrationErrorMessage"

I hit my endpoint again after deploying the change and went straight to the access logs. There was the 500 but there was no integration error message. What is going on?

2020-12-18T10:07:44.913-08:00   8.45.151.32 - - [18/Dec/2020:18:07:44 +0000] "GET GET /leagues HTTP/1.1" 500 35 XwptqhuYPHcESYw= -

The troubleshooting docs tell you that the common reason for this error has to do with the fact that the resource-based policy on the Lambda function that is being called may not have granted the API Gateway the lambda:InvokeFunction permission. The resource-based policy on the Lambda function defines who/what can invoke this function.

So I checked the resource-based policy on the Lambda function. It looked absolutely flawless, unsurprisingly.

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "My-API-LambdaRootGetPermission-15CAEMBXBCPZF",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-west-2:<account-id>:function:My-API-Lambda-EUJ1YGFN0R2W",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:us-west-2:<account-id>:by6a0ehwm6/*/GET/myendpoint"
        }
      }
    }
  ]
}

OK, so that all looked alright. So why the Internal Server Error?

Remember, there are actually two functions in play here. There is the Custom Authorizer function that sits in front of the endpoint. It authorizes the request by checking the token with Auth0. The second function is the actual API function that executes logic. We confirmed that the second function doesn't appear to have any permission issues. But remember, we have no logs coming from either function. So neither function is being invoked by the API Gateway.

The keyword here is invoked. Neither function has been invoked and thus there is no logs.

The Custom Authorizer function sits in front of the actual API function. So the reason for the Internal Server Error must live within the Custom Authorizer. Furthermore, because it hasn't been invoked I can deduce that the problem is likely the resource-based policy on the Custom Authorizer.

It turns out, for my scenario, the Internal Server Error was because there was no resource-based policy on the Custom Authorizer that allowed the API Gateway endpoint to invoke it.

The Custom Authorizer is a separate function. So it was deployed as a standalone function, not hooked up to any API Gateway endpoint directly. Because of this fact it never got any kind of resource-based policy. The solution to my specific problem was to add the following resource-based policy to my Custom Authorizer function.

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "CustomAuthorizerAPIGateway15CAEMBXBCPZF",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-west-2:<account-id>:function:my-custom-authorizer"
    }
  ]
}

This bit of CloudFormation grants API Gateway the ability to invoke my Custom Authorizer function. This allows me to develop many APIs and each of them can use this common authorizer without running into this permission problem again.

Conclusion

This was a bit tricky to debug on the fly. The reason being is because of the error you are likely Googling when you encounter this. If you look up API Gateway Internal Server Error you will most likely end up on these troubleshooting docs I linked to earlier. But these docs are assuming that your API Gateway endpoint is directly linked to the Lambda function (i.e. there is no custom authorizer in the middle).

These are valid docs for any situation including ours. But those docs won't show you the error happening with your authorizer. That variable only shows you the error with the Lambda function the API endpoint is trying to call. $context.integrationErrorMessage shows you the integration error but it won't show you any errors up before that. When you have a custom authorizer involved you should also add $context.authorizer.error to your access logs format as mentioned at the very bottom of this documentation.

In hindsight, this all seems obvious. But during the moment it felt a lot like trying to find a needle in a haystack. Hopefully, this post helps point you in the right direction if you encounter something similar getting an HTTP API endpoint working with a custom authorizer in the middle.

Want to check out my other projects?

I am a huge fan of the DEV community. If you have any questions or want to chat about different ideas relating to refactoring, reach out on Twitter.

Outside of blogging, I created a Learn AWS By Using It course. In the course, we focus on learning Amazon Web Services by actually using it to host, secure, and deliver static websites. It’s a simple problem, with many solutions, but it’s perfect for ramping up your understanding of AWS. I recently added two new bonus chapters to the course that focus on Infrastructure as Code and Continuous Deployment.

I also curate my own weekly newsletter. The Learn By Doing newsletter is packed full of awesome cloud, coding, and DevOps articles each week. Sign up to get it in your inbox.