2

I am using a C# lambda to integrate with API gateway. I want API gateway to return proper error codes like 400, 404, 500 etc.

API gateway module tf file

provider "aws" {
  version = "<= 2.70.0"
  region = "${var.aws_region}"
  profile = "${var.aws_profile}"
}

terraform {
  # The configuration for this backend will be filled in by Terragrunt
  backend "s3" {}
}

data "terraform_remote_state" "api_state" {
  backend = "s3"

  config {
    region = "${var.aws_region}"
    profile = "${var.aws_profile}"
    bucket = "${var.s3_remote_state_bucket_name}"
    key = "${var.s3_remote_state_key_name_api}"
  }
}

data "terraform_remote_state" "resource_state"{
  backend = "s3"
  config {
    region = "${var.aws_region}"
    profile = "${var.aws_profile}"
    bucket = "${var.s3_remote_state_bucket_name}"
    key = "${var.s3_remote_state_key_name_resource}"
  }
}

data "terraform_remote_state" "lambda_alias"{
  backend = "s3"

  config {
    region = "${var.aws_region}"
    profile = "${var.aws_profile}"
    bucket = "${var.s3_remote_state_bucket_name}"
    key = "${var.s3_remote_state_key_name_lambda}"
  }
}

resource "aws_api_gateway_method" "http-method" {
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  http_method = "GET"
  authorization = "CUSTOM"
  authorizer_id = "${data.terraform_remote_state.api_state.Authorizers[var.Authorizer]}"
  request_parameters = "${var.api_request_params_required}"
}

resource "aws_api_gateway_integration" "integration_GET" {
  rest_api_id             = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  resource_id             = "${data.terraform_remote_state.resource_state.api_resource_id}"
  http_method             = "${aws_api_gateway_method.http-method.http_method}"
  integration_http_method = "POST"
  type                    = "AWS"
  uri                     = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${data.terraform_remote_state.lambda_alias.alias_lambda_arn}/invocations"
  passthrough_behavior = "WHEN_NO_TEMPLATES"
  request_templates = {
    "application/json" = "${file("api_gateway_body_mapping.template")}"
  }
}

resource "aws_api_gateway_model" "error_response" {
  rest_api_id  = "${aws_api_gateway_rest_api.api_gateway_rest_api.id}"
  name         = "ErrorResponse"
  description  = "The error respone object for all endpoints"
  content_type = "application/json"

  schema = <<EOF
  {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type" : "object",
    "properties": {
          "body": {
              "type": "string"
          },
          "statusCode" : {
              "type": "number"
          }
      }
  }
EOF
}

resource "aws_api_gateway_method_response" "method_response" {
  depends_on = ["aws_api_gateway_method.http-method"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "method_bad_request" {
  depends_on = ["aws_api_gateway_method.http-method"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "400"
  response_models {
    "application/json" = "${aws_api_gateway_model.error_response}"
  }
}

resource "aws_api_gateway_method_response" "method_not_found" {
  depends_on = ["aws_api_gateway_method.http-method"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "404"
  response_models {
    "application/json" = "${aws_api_gateway_model.error_response}"
  }
}

resource "aws_api_gateway_method_response" "method_error" {
  depends_on = ["aws_api_gateway_method.http-method"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "500"
  response_models {
    "application/json" = "${aws_api_gateway_model.error_response}"
  }
}

resource "aws_api_gateway_integration_response" "get_integration_response_success" {
  depends_on = ["aws_api_gateway_method_response.method_response", "aws_api_gateway_integration.integration_GET"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "${aws_api_gateway_method_response.method_response.status_code}"
  response_templates {
    "application/json" = ""
  }
}

resource "aws_api_gateway_integration_response" "get_integration_response_error" {
  depends_on = ["aws_api_gateway_method_response.method_error", "aws_api_gateway_integration.integration_GET"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "${aws_api_gateway_method_response.method_error.status_code}"
  selection_pattern = ".*statusCode['\"]\\s*:\\s*['\"]?500.*"
  response_templates {
    "application/json"="${file("api_gateway_exception_mapping.template")}"
  }
}

resource "aws_api_gateway_integration_response" "get_integration_response_bad_request" {
  depends_on = ["aws_api_gateway_method_response.method_bad_request", "aws_api_gateway_integration.integration_GET"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "${aws_api_gateway_method_response.method_bad_request.status_code}"
  selection_pattern = ".*statusCode['\"]\\s*:\\s*['\"]?400.*"
  response_templates {
    "application/json"="${file("api_gateway_exception_mapping.template")}"
  }
}

resource "aws_api_gateway_integration_response" "get_integration_response_not_found" {
  depends_on = ["aws_api_gateway_method_response.method_not_found", "aws_api_gateway_integration.integration_GET"]
  http_method = "${aws_api_gateway_method.http-method.http_method}"
  resource_id = "${data.terraform_remote_state.resource_state.api_resource_id}"
  rest_api_id = "${data.terraform_remote_state.api_state.api_gateway_rest_api_id}"
  status_code = "${aws_api_gateway_method_response.method_not_found.status_code}"
  selection_pattern = ".*statusCode['\"]\\s*:\\s*['\"]?404.*"
  response_templates {
    "application/json"="{}"
  }
}

api_gateway_exception_mapping.template:

#set($inputRoot = $util.parseJson($input.path('$.errorMessage')))
{
"Error":"$inputRoot.body"
}

The integration response mapping is as in below snapshot

Integration Response for the API

We have create APIs with lambda integration in python where I threw a custom APIException as below and it worked.

class ApiException(Exception):
    """Our custom APIException class which derives from the built-in Exception class"""

    def __init__(self, status_code, message: str, **kwargs):
        self.status_code = status_code
        self.message = message
        kwargs["statusCode"] = status_code
        kwargs["body"] = message
        super().__init__(json.dumps(kwargs))

Inside the lambda handler:

from .utils import ApiException
def lambda_handler(event, context):
    try:
        """
        CODE FOR LAMBDA HANDLER
        """
    except Exception:
        ex = ApiException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
                           message='Internal Error')
        print("exception string: %s", ex)
        raise ApiException(
            status_code=500,
            message='Internal server error')

When I logged the exception, I got the following output

{
    "statusCode": 500,
    "body": "Internal Server Error"
}

I also referred this stackoverflow answer on how to get proper error codes in API Gateway response. I modified that a little bit to throw an exception instead of just returning a json with statusCode, response body and headers as I am not using AWS_PROXY integration type

APIException.cs

public class APIException : Exception
{
    public int statusCode;
    public string body;
    public APIException() : base() {}
    public APIException(int statusCode, string message): base(message) {
        this.statusCode = statusCode;
        JObject json = JObject.Parse(message);
        this.body = json["body"].ToString();
    }
}

Lambda handler:

namespace LambdaFunction
{
    public class Function
    {
        public async Task<JObject> FunctionHandler(JObject events, ILambdaContext context)
        {
           try
            {
                ValidateQueryParams(events, context);
                JObject response = JObject.Parse(@"{
                    'mesage': 'success',
                }");

                return response;
            }
            catch(HttpListenerException ex)
            {
                string err = (new JObject(
                    new JProperty("statusCode", ex.ErrorCode),
                    new JProperty("body", ex.Message)
                )).ToString();
                return new APIException(ex.ErrorCode, err);
            }
            catch(Exception ex)
            {
                int err_code = (int)HttpStatusCode.InternalServerError
                string err = (new JObject(
                    new JProperty("statusCode", err_code),
                    new JProperty("body", "Internal Server Error")
                )).ToString();
                var err_ex = new APIException(err_code, err);
                context.Logger.LogLine("Unhandled exception occurred: " + ex.ToString());
                return err_ex;
            }
        }
    }
}

I've logged the exception before throwing it to see what we are getting and this is what I got

{
    "statusCode": 500,
    "body": "Internal Server Error",
    "StackTrace": null,
    "Message": "{\n  \"statusCode\": 500,\n  \"body\": \"Internal Server Error\"\n}",
    "Data": {},
    "InnerException": null,
    "HelpLink": null,
    "Source": null,
    "HResult": -2146233088
}

But with the above code, I'm still just getting response code as 200 with below response body

{
    "errorType": "APIException",
    "errorMessage": "{\n  \"statusCode\": 500,\n  \"body\": \"Internal Server Error\"\n}",
    "stackTrace": [
        "..."
    ]
}

I am not sure where I am going wrong. Any help will be appreciated. Thanks.

troglodyte07
  • 3,598
  • 10
  • 42
  • 66

1 Answers1

0

I've figured out the issue. C#'s .ToString() function which converts JObject into a string is formatting the string and adding new line characters \n by default. But the regex used in API gateway to identify error codes does not consider new line characters. The fix was pretty simple. We need to tell the .ToString() function that it shouldn't do any formatting. So, instead of

new JObject(
    new JProperty("statusCode", err_code),
    new JProperty("body", "Internal Server Error")
)).ToString();

You need to do

new JObject(
    new JProperty("statusCode", err_code),
    new JProperty("body", "Internal Server Error")
)).ToString(Formatting.None);

And it worked.

troglodyte07
  • 3,598
  • 10
  • 42
  • 66