Repository: localstack/localstack

Issue:
Invoking a lambda using a function url always returns 200
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Given the following javascript lambda function
```javascript
exports.main = async (event) => {
  console.log('Hello World');
  return {
    statusCode: '302',
    body: {},
    headers: {
      Location: 'https://example.com'
    }
  };
}
```

When deploying to local stack and adding a function url, the url returns the correct `body` and `headers` but it will return a 200 status code.

In a real aws environment, an actual 302 is returned.

### Expected Behavior

The correct status code is returned

### How are you starting LocalStack?

With a docker-compose file

### Steps To Reproduce

My docker compose file

```yaml
version: '3.4'
services:
  localstack:
    image: localstack/localstack:2.0.2
    environment:
      - SERVICES=dynamodb,lambda
    ports:
      - '4566:4566'
    expose:
      - '4566'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
```

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

deploying using terraform

```hcl
data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "iam_for_lambda" {
  name               = "iam_for_lambda"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "../index.js"
  output_path = local.archive_file
}

resource "aws_lambda_function" "redirect_lambda" {
  filename      = local.archive_file
  function_name = "redirects"
  role          = aws_iam_role.iam_for_lambda.arn
  handler       = "index.main"

  source_code_hash = data.archive_file.lambda.output_base64sha256

  runtime = "nodejs18.x"

  environment {
    variables = {
      foo = "bar"
    }
  }
}

resource "aws_lambda_function_url" "lambda_url" {
  function_name      = aws_lambda_function.redirect_lambda.function_name
  authorization_type = "NONE"
}
```


### Environment

```markdown
- OS: docker
- LocalStack: latest
```


### Anything else?

_No response_


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).
I wrote a [blog post](https://www.willmunn.xyz/terraform/aws/devops/lambda/ioc/localstack/2023/04/29/using-localstack-to-test-terraform.html) explaining the steps I took in more detail if that helps
I can confirm this bug is happening. Any updates on this?
Hello, I have encountered the same error and would like to provide additional information. If the exceptions are not handled, it results in an error 502. However, with proper exception handling, the returned code is always 200. Regarding the exception messages, they are consistently propagated correctly.
having a similar issue as well in my docker compose file when trying the invoke my lambda with the aws cli

`aws --endpoint-url=http://localhost:4566 lambda invoke --function-name function-handler-name --payload '{ "name": "Bob" }' response.json`

I would also keep getting a code 200 after running this command.

This is my docker compose file
```
version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
    environment:
      - DEBUG=${DEBUG-}
      - DOCKER_HOST=unix:///var/run/docker.sock
      - AWS_DEFAULT_REGION = ap-southeast-1
      - EDGE_PORT = 4566
      - SERVICES=lambda,s3,cloudformation,sts,apigateway,iam,route53,dynamodb
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"
```
Hello 👋! It looks like this issue hasn’t been active in longer than five months. We encourage you to check if this is still an issue in the latest release. In the absence of more information, we will be closing this issue soon. If you find that this is still a problem, please feel free to provide a comment or upvote with a reaction on the initial post to prevent automatic closure. If the issue is already closed, please feel free to open a new one.

Resolving patch:
diff --git a/localstack/services/lambda_/urlrouter.py b/localstack/services/lambda_/urlrouter.py
--- a/localstack/services/lambda_/urlrouter.py
+++ b/localstack/services/lambda_/urlrouter.py
@@ -192,6 +192,8 @@ def lambda_result_to_response(result: InvocationResult):
         # if it's a dict it might be a proper response
         if isinstance(parsed_result.get("headers"), dict):
             response.headers.update(parsed_result.get("headers"))
+        if "statusCode" in parsed_result:
+            response.status_code = int(parsed_result["statusCode"])
         if "body" not in parsed_result:
             # TODO: test if providing a status code but no body actually works
             response.data = original_payload


Test patch:
diff --git a/tests/aws/services/lambda_/functions/lambda_echo_json_body.py b/tests/aws/services/lambda_/functions/lambda_echo_json_body.py
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/lambda_/functions/lambda_echo_json_body.py
@@ -0,0 +1,24 @@
+"""
+Interprets the body of the lambda event as a JSON document and returns it. This allows you to
+dynamically simulate functions that look like::
+
+    def handler(event, context):
+        return {
+           "statusCode": 201,
+            "headers": {
+                "Content-Type": "application/json",
+                "My-Custom-Header": "Custom Value"
+            },
+            "body": json.dumps({
+                "message": "Hello, world!"
+            }),
+            "isBase64Encoded": False,
+        }
+"""
+import json
+
+
+def handler(event, context):
+    # Just print the event that was passed to the Lambda
+    print(json.dumps(event))
+    return json.loads(event["body"])
diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py
--- a/tests/aws/services/lambda_/test_lambda.py
+++ b/tests/aws/services/lambda_/test_lambda.py
@@ -14,6 +14,7 @@
 from typing import Dict, TypeVar
 
 import pytest
+import requests
 from botocore.config import Config
 from botocore.response import StreamingBody
 
@@ -47,6 +48,7 @@
 THIS_FOLDER = os.path.dirname(os.path.realpath(__file__))
 TEST_LAMBDA_PYTHON = os.path.join(THIS_FOLDER, "functions/lambda_integration.py")
 TEST_LAMBDA_PYTHON_ECHO = os.path.join(THIS_FOLDER, "functions/lambda_echo.py")
+TEST_LAMBDA_PYTHON_ECHO_JSON_BODY = os.path.join(THIS_FOLDER, "functions/lambda_echo_json_body.py")
 TEST_LAMBDA_PYTHON_REQUEST_ID = os.path.join(THIS_FOLDER, "functions/lambda_request_id.py")
 TEST_LAMBDA_PYTHON_ECHO_VERSION_ENV = os.path.join(
     THIS_FOLDER, "functions/lambda_echo_version_env.py"
@@ -829,6 +831,55 @@ def test_lambda_url_echo_invoke(self, create_lambda_function, snapshot, aws_clie
         assert "Body" not in event
         assert event["isBase64Encoded"] is False
 
+    @markers.aws.validated
+    def test_lambda_url_headers_and_status(self, create_lambda_function, aws_client):
+        function_name = f"test-fnurl-echo-{short_uid()}"
+
+        create_lambda_function(
+            func_name=function_name,
+            zip_file=testutil.create_zip_file(TEST_LAMBDA_PYTHON_ECHO_JSON_BODY, get_content=True),
+            runtime=Runtime.python3_10,
+            handler="lambda_echo_json_body.handler",
+        )
+        url_config = aws_client.lambda_.create_function_url_config(
+            FunctionName=function_name,
+            AuthType="NONE",
+        )
+        aws_client.lambda_.add_permission(
+            FunctionName=function_name,
+            StatementId="urlPermission",
+            Action="lambda:InvokeFunctionUrl",
+            Principal="*",
+            FunctionUrlAuthType="NONE",
+        )
+
+        url = f"{url_config['FunctionUrl']}custom_path/extend?test_param=test_value"
+
+        event = {
+            "statusCode": 201,
+            "headers": {
+                "Content-Type": "application/json",
+                "My-Custom-Header": "Custom Value",
+            },
+            "body": json.dumps({"message": "hello-world"}),
+            "isBase64Encoded": False,
+        }
+        result = requests.post(url, json=event)
+        assert result.json() == {"message": "hello-world"}
+        assert result.status_code == 201
+        assert "my-custom-header" in result.headers
+        assert result.headers["my-custom-header"] == "Custom Value"
+
+        # try with string status code
+        event = {
+            "statusCode": "418",
+            "body": "i'm a teapot",
+            "isBase64Encoded": False,
+        }
+        result = requests.post(url, json=event)
+        assert result.text == "i'm a teapot"
+        assert result.status_code == 418
+
     @markers.aws.validated
     def test_lambda_url_invocation_exception(self, create_lambda_function, snapshot, aws_client):
         # TODO: extend tests
diff --git a/tests/aws/services/lambda_/test_lambda.validation.json b/tests/aws/services/lambda_/test_lambda.validation.json
--- a/tests/aws/services/lambda_/test_lambda.validation.json
+++ b/tests/aws/services/lambda_/test_lambda.validation.json
@@ -140,6 +140,9 @@
   "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke": {
     "last_validated_date": "2023-11-20T21:05:40+00:00"
   },
+  "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_headers_and_status": {
+    "last_validated_date": "2024-02-04T16:00:29+00:00"
+  },
   "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[boolean]": {
     "last_validated_date": "2023-11-20T21:05:35+00:00"
   },
@@ -182,4 +185,4 @@
   "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke_url": {
     "last_validated_date": "2023-11-20T21:08:49+00:00"
   }
-}
+}
\ No newline at end of file