Spent more than half a day, struggled with this error when I was testing an async lambda on my local.

Error

I created a lambda which had an async implementation, but when I deployed it on lambda, and used function url to access it.

It started giving an error in response

{"__type": "InternalError", "message": "exception while calling lambda with unknown operation: 'str' object has no attribute 'get'"}

Steps to recreate

Let us create a lambda with simple wait function.

export const sleep = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

exports.handler = async (event, context) => {
    await sleep(10)

    return {
        statusCode: 200,
        headers: {'Content-Type': 'application/json'},
        body: "Hello World",
    };
};

Deploying to localstack

So we have our code ready to deploy, I will use terraform to deploy the code to localstack. But first make sure, you have your localstack setup, follow the instructions if you have not.

Terraform

Now we will create provider.tf to point our aws provider to localstack.

terraform {
  required_version = "~> 1.3.3"
}

provider "aws" {
  access_key                  = "test"
  secret_key                  = "test"
  region                      = "us-east-1"
  s3_force_path_style         = false
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    apigatewayv2   = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    ec2            = "http://localhost:4566"
    es             = "http://localhost:4566"
    elasticache    = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    rds            = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    route53        = "http://localhost:4566"
    s3             = "http://s3.localhost.localstack.cloud:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}


As we have the provider setup, lets deploy the lambda.

resource "aws_iam_role" "iam_for_lambda_fix" {
  name = "iam_for_lambda_fix"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "./index.js"
  output_path = "lambda.zip"

}

resource "aws_lambda_function" "lambda" {
  filename         = "lambda.zip"
  function_name    = "lambda_fix"
  role             = aws_iam_role.iam_for_lambda_fix.arn
  handler          = "./index.handler"
  source_code_hash = "${filebase64sha256("./index.js")}"
  runtime          = "nodejs16.x"
  timeout = 30

  depends_on = [data.archive_file.lambda_zip]
}

resource "aws_lambda_function_url" "function_url" {

  function_name = aws_lambda_function.lambda.function_name
  authorization_type = "NONE"

}

output "url" {
  value = aws_lambda_function_url.function_url
}

Here, we have created an iam policy, then archived the js file, and deploy a function.

All set, now to run this application.

$ terraform init
$ terraform plan
$ terraform apply --auto-approve

Once the commands are executed.

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

url = {
  "authorization_type" = "NONE"
  "cors" = tolist([])
  "function_arn" = "arn:aws:lambda:us-east-1:000000000000:function:lambda_fix"
  "function_name" = "lambda_fix"
  "function_url" = "http://cdd7e6500eb91965c99dc0eb42b1613a.lambda-url.us-east-1.localhost.localstack.cloud:4566/"
  "id" = "lambda_fix"
  "qualifier" = ""
  "timeouts" = null /* object */
  "url_id" = "cdd7e6500eb91965c99dc0eb42b1613a"
}

now if we do curl, we will get the error

$ curl http://cdd7e6500eb91965c99dc0eb42b1613a.lambda-url.us-east-1.localhost.localstack.cloud:4566/

{"__type": "InternalError", "message": "exception while calling lambda with unknown operation: the JSON object must be str, bytes or bytearray, not Response"}% 

But if you deploy the same lambda to aws , it works fine. To my surprise if I remove the async operation, the application works fine on localstack as well.

Fix

The fix is rather simple, for unknown reason, localstack is unable to convert the async/await to promise result.

So to fix the above issue lets do a simple modification.


export const sleep = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

exports.handler = async (event, context) => {
    await sleep(10)

    return Promise.resolve({
        statusCode: 200,
        headers: {'Content-Type': 'application/json'},
        body: "Hello World",
    });
};

We just add the Promise.resolve() explicitly. And that solve the issue with localstack function url

Apply our change again using terraform apply & finally do a curl.

$ curl http://cdd7e6500eb91965c99dc0eb42b1613a.lambda-url.us-east-1.localhost.localstack.cloud:4566/
Hello World

You can find the code here.

Categories: ,

Updated:

Kumar Rohit
WRITTEN BY

Kumar Rohit

I like long drives, bike trip & good food. I have passion for coding, especially for Clean-Code.

Leave a comment