We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `localstack/services/lambda_/urlrouter.py`
Content:
```
1 """Routing for Lambda function URLs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html"""
2 import base64
3 import json
4 import logging
5 import urllib
6 from datetime import datetime
7 from http import HTTPStatus
8 
9 from localstack.aws.api import HttpResponse
10 from localstack.aws.api.lambda_ import InvocationType
11 from localstack.aws.protocol.serializer import gen_amzn_requestid
12 from localstack.http import Request, Router
13 from localstack.http.dispatcher import Handler
14 from localstack.services.lambda_.api_utils import FULL_FN_ARN_PATTERN
15 from localstack.services.lambda_.invocation.lambda_models import InvocationResult
16 from localstack.services.lambda_.invocation.lambda_service import LambdaService
17 from localstack.services.lambda_.invocation.models import lambda_stores
18 from localstack.utils.aws.request_context import AWS_REGION_REGEX
19 from localstack.utils.strings import long_uid, to_bytes, to_str
20 from localstack.utils.time import TIMESTAMP_READABLE_FORMAT, mktime, timestamp
21 from localstack.utils.urls import localstack_host
22 
23 LOG = logging.getLogger(__name__)
24 
25 
26 class FunctionUrlRouter:
27     router: Router[Handler]
28     lambda_service: LambdaService
29 
30     def __init__(self, router: Router[Handler], lambda_service: LambdaService):
31         self.router = router
32         self.registered = False
33         self.lambda_service = lambda_service
34 
35     def register_routes(self) -> None:
36         if self.registered:
37             LOG.debug("Skipped Lambda URL route registration (routes already registered).")
38             return
39         self.registered = True
40 
41         LOG.debug("Registering parameterized Lambda routes.")
42 
43         self.router.add(
44             "/",
45             host=f"<api_id>.lambda-url.<regex('{AWS_REGION_REGEX}'):region>.<regex('.*'):server>",
46             endpoint=self.handle_lambda_url_invocation,
47             defaults={"path": ""},
48         )
49         self.router.add(
50             "/<path:path>",
51             host=f"<api_id>.lambda-url.<regex('{AWS_REGION_REGEX}'):region>.<regex('.*'):server>",
52             endpoint=self.handle_lambda_url_invocation,
53         )
54 
55     def handle_lambda_url_invocation(
56         self, request: Request, api_id: str, region: str, **url_params: dict[str, str]
57     ) -> HttpResponse:
58         response = HttpResponse(headers={"Content-type": "application/json"})
59 
60         lambda_url_config = None
61         try:
62             for account_id in lambda_stores.keys():
63                 store = lambda_stores[account_id][region]
64                 for fn in store.functions.values():
65                     for url_config in fn.function_url_configs.values():
66                         if url_config.url_id == api_id:
67                             lambda_url_config = url_config
68         except IndexError as e:
69             LOG.warning(f"Lambda URL ({api_id}) not found: {e}")
70             response.set_json({"Message": None})
71             response.status = "404"
72             return response
73 
74         event = event_for_lambda_url(
75             api_id, request.full_path, request.data, request.headers, request.method
76         )
77 
78         match = FULL_FN_ARN_PATTERN.search(lambda_url_config.function_arn).groupdict()
79 
80         result = self.lambda_service.invoke(
81             function_name=match.get("function_name"),
82             qualifier=match.get("qualifier"),
83             account_id=match.get("account_id"),
84             region=match.get("region_name"),
85             invocation_type=InvocationType.RequestResponse,
86             client_context="{}",  # TODO: test
87             payload=to_bytes(json.dumps(event)),
88             request_id=gen_amzn_requestid(),
89         )
90         if result.is_error:
91             response = HttpResponse("Internal Server Error", HTTPStatus.BAD_GATEWAY)
92         else:
93             response = lambda_result_to_response(result)
94         return response
95 
96 
97 def event_for_lambda_url(api_id: str, path: str, data, headers, method: str) -> dict:
98     raw_path = path.split("?")[0]
99     raw_query_string = path.split("?")[1] if len(path.split("?")) > 1 else ""
100     query_string_parameters = (
101         {} if not raw_query_string else dict(urllib.parse.parse_qsl(raw_query_string))
102     )
103 
104     now = datetime.utcnow()
105     readable = timestamp(time=now, format=TIMESTAMP_READABLE_FORMAT)
106     if not any(char in readable for char in ["+", "-"]):
107         readable += "+0000"
108 
109     source_ip = headers.get("Remote-Addr", "")
110     request_context = {
111         "accountId": "anonymous",
112         "apiId": api_id,
113         "domainName": headers.get("Host", ""),
114         "domainPrefix": api_id,
115         "http": {
116             "method": method,
117             "path": raw_path,
118             "protocol": "HTTP/1.1",
119             "sourceIp": source_ip,
120             "userAgent": headers.get("User-Agent", ""),
121         },
122         "requestId": long_uid(),
123         "routeKey": "$default",
124         "stage": "$default",
125         "time": readable,
126         "timeEpoch": mktime(ts=now, millis=True),
127     }
128 
129     content_type = headers.get("Content-Type", "").lower()
130     content_type_is_text = any(text_type in content_type for text_type in ["text", "json", "xml"])
131 
132     is_base64_encoded = not (data.isascii() and content_type_is_text) if data else False
133     body = base64.b64encode(data).decode() if is_base64_encoded else data
134     if isinstance(body, bytes):
135         body = to_str(body)
136 
137     ignored_headers = ["connection", "x-localstack-tgt-api", "x-localstack-request-url"]
138     event_headers = {k.lower(): v for k, v in headers.items() if k.lower() not in ignored_headers}
139 
140     event_headers.update(
141         {
142             "x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256",
143             "x-amzn-tls-version": "TLSv1.2",
144             "x-forwarded-proto": "http",
145             "x-forwarded-for": source_ip,
146             "x-forwarded-port": str(localstack_host().port),
147         }
148     )
149 
150     event = {
151         "version": "2.0",
152         "routeKey": "$default",
153         "rawPath": raw_path,
154         "rawQueryString": raw_query_string,
155         "headers": event_headers,
156         "queryStringParameters": query_string_parameters,
157         "requestContext": request_context,
158         "body": body,
159         "isBase64Encoded": is_base64_encoded,
160     }
161 
162     if not data:
163         event.pop("body")
164 
165     return event
166 
167 
168 def lambda_result_to_response(result: InvocationResult):
169     response = HttpResponse()
170 
171     # Set default headers
172     response.headers.update(
173         {
174             "Content-Type": "application/json",
175             "Connection": "keep-alive",
176             "x-amzn-requestid": result.request_id,
177             "x-amzn-trace-id": long_uid(),  # TODO: get the proper trace id here
178         }
179     )
180 
181     original_payload = to_str(result.payload)
182     parsed_result = json.loads(original_payload)
183 
184     # patch to fix whitespaces
185     # TODO: check if this is a downstream issue of invocation result serialization
186     original_payload = json.dumps(parsed_result, separators=(",", ":"))
187 
188     if isinstance(parsed_result, str):
189         # a string is a special case here and is returned as-is
190         response.data = parsed_result
191     elif isinstance(parsed_result, dict):
192         # if it's a dict it might be a proper response
193         if isinstance(parsed_result.get("headers"), dict):
194             response.headers.update(parsed_result.get("headers"))
195         if "body" not in parsed_result:
196             # TODO: test if providing a status code but no body actually works
197             response.data = original_payload
198         elif isinstance(parsed_result.get("body"), dict):
199             response.data = json.dumps(parsed_result.get("body"))
200         elif parsed_result.get("isBase64Encoded", False):
201             body_bytes = to_bytes(to_str(parsed_result.get("body", "")))
202             decoded_body_bytes = base64.b64decode(body_bytes)
203             response.data = decoded_body_bytes
204         else:
205             response.data = parsed_result.get("body")
206     else:
207         response.data = original_payload
208 
209     return response
210
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
         # 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

{"golden_diff": "diff --git a/localstack/services/lambda_/urlrouter.py b/localstack/services/lambda_/urlrouter.py\n--- a/localstack/services/lambda_/urlrouter.py\n+++ b/localstack/services/lambda_/urlrouter.py\n@@ -192,6 +192,8 @@\n         # if it's a dict it might be a proper response\n         if isinstance(parsed_result.get(\"headers\"), dict):\n             response.headers.update(parsed_result.get(\"headers\"))\n+        if \"statusCode\" in parsed_result:\n+            response.status_code = int(parsed_result[\"statusCode\"])\n         if \"body\" not in parsed_result:\n             # TODO: test if providing a status code but no body actually works\n             response.data = original_payload\n", "issue": "Invoking a lambda using a function url always returns 200\n### Is there an existing issue for this?\n\n- [X] I have searched the existing issues\n\n### Current Behavior\n\nGiven the following javascript lambda function\r\n```javascript\r\nexports.main = async (event) => {\r\n  console.log('Hello World');\r\n  return {\r\n    statusCode: '302',\r\n    body: {},\r\n    headers: {\r\n      Location: 'https://example.com'\r\n    }\r\n  };\r\n}\r\n```\r\n\r\nWhen 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.\r\n\r\nIn a real aws environment, an actual 302 is returned.\n\n### Expected Behavior\n\nThe correct status code is returned\n\n### How are you starting LocalStack?\n\nWith a docker-compose file\n\n### Steps To Reproduce\n\nMy docker compose file\r\n\r\n```yaml\r\nversion: '3.4'\r\nservices:\r\n  localstack:\r\n    image: localstack/localstack:2.0.2\r\n    environment:\r\n      - SERVICES=dynamodb,lambda\r\n    ports:\r\n      - '4566:4566'\r\n    expose:\r\n      - '4566'\r\n    volumes:\r\n      - /var/run/docker.sock:/var/run/docker.sock\r\n```\r\n\r\n#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)\r\n\r\ndeploying using terraform\r\n\r\n```hcl\r\ndata \"aws_iam_policy_document\" \"assume_role\" {\r\n  statement {\r\n    effect = \"Allow\"\r\n\r\n    principals {\r\n      type        = \"Service\"\r\n      identifiers = [\"lambda.amazonaws.com\"]\r\n    }\r\n\r\n    actions = [\"sts:AssumeRole\"]\r\n  }\r\n}\r\n\r\nresource \"aws_iam_role\" \"iam_for_lambda\" {\r\n  name               = \"iam_for_lambda\"\r\n  assume_role_policy = data.aws_iam_policy_document.assume_role.json\r\n}\r\n\r\ndata \"archive_file\" \"lambda\" {\r\n  type        = \"zip\"\r\n  source_file = \"../index.js\"\r\n  output_path = local.archive_file\r\n}\r\n\r\nresource \"aws_lambda_function\" \"redirect_lambda\" {\r\n  filename      = local.archive_file\r\n  function_name = \"redirects\"\r\n  role          = aws_iam_role.iam_for_lambda.arn\r\n  handler       = \"index.main\"\r\n\r\n  source_code_hash = data.archive_file.lambda.output_base64sha256\r\n\r\n  runtime = \"nodejs18.x\"\r\n\r\n  environment {\r\n    variables = {\r\n      foo = \"bar\"\r\n    }\r\n  }\r\n}\r\n\r\nresource \"aws_lambda_function_url\" \"lambda_url\" {\r\n  function_name      = aws_lambda_function.redirect_lambda.function_name\r\n  authorization_type = \"NONE\"\r\n}\r\n```\r\n\n\n### Environment\n\n```markdown\n- OS: docker\r\n- LocalStack: latest\n```\n\n\n### Anything else?\n\n_No response_\n", "before_files": [{"content": "\"\"\"Routing for Lambda function URLs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html\"\"\"\nimport base64\nimport json\nimport logging\nimport urllib\nfrom datetime import datetime\nfrom http import HTTPStatus\n\nfrom localstack.aws.api import HttpResponse\nfrom localstack.aws.api.lambda_ import InvocationType\nfrom localstack.aws.protocol.serializer import gen_amzn_requestid\nfrom localstack.http import Request, Router\nfrom localstack.http.dispatcher import Handler\nfrom localstack.services.lambda_.api_utils import FULL_FN_ARN_PATTERN\nfrom localstack.services.lambda_.invocation.lambda_models import InvocationResult\nfrom localstack.services.lambda_.invocation.lambda_service import LambdaService\nfrom localstack.services.lambda_.invocation.models import lambda_stores\nfrom localstack.utils.aws.request_context import AWS_REGION_REGEX\nfrom localstack.utils.strings import long_uid, to_bytes, to_str\nfrom localstack.utils.time import TIMESTAMP_READABLE_FORMAT, mktime, timestamp\nfrom localstack.utils.urls import localstack_host\n\nLOG = logging.getLogger(__name__)\n\n\nclass FunctionUrlRouter:\n    router: Router[Handler]\n    lambda_service: LambdaService\n\n    def __init__(self, router: Router[Handler], lambda_service: LambdaService):\n        self.router = router\n        self.registered = False\n        self.lambda_service = lambda_service\n\n    def register_routes(self) -> None:\n        if self.registered:\n            LOG.debug(\"Skipped Lambda URL route registration (routes already registered).\")\n            return\n        self.registered = True\n\n        LOG.debug(\"Registering parameterized Lambda routes.\")\n\n        self.router.add(\n            \"/\",\n            host=f\"<api_id>.lambda-url.<regex('{AWS_REGION_REGEX}'):region>.<regex('.*'):server>\",\n            endpoint=self.handle_lambda_url_invocation,\n            defaults={\"path\": \"\"},\n        )\n        self.router.add(\n            \"/<path:path>\",\n            host=f\"<api_id>.lambda-url.<regex('{AWS_REGION_REGEX}'):region>.<regex('.*'):server>\",\n            endpoint=self.handle_lambda_url_invocation,\n        )\n\n    def handle_lambda_url_invocation(\n        self, request: Request, api_id: str, region: str, **url_params: dict[str, str]\n    ) -> HttpResponse:\n        response = HttpResponse(headers={\"Content-type\": \"application/json\"})\n\n        lambda_url_config = None\n        try:\n            for account_id in lambda_stores.keys():\n                store = lambda_stores[account_id][region]\n                for fn in store.functions.values():\n                    for url_config in fn.function_url_configs.values():\n                        if url_config.url_id == api_id:\n                            lambda_url_config = url_config\n        except IndexError as e:\n            LOG.warning(f\"Lambda URL ({api_id}) not found: {e}\")\n            response.set_json({\"Message\": None})\n            response.status = \"404\"\n            return response\n\n        event = event_for_lambda_url(\n            api_id, request.full_path, request.data, request.headers, request.method\n        )\n\n        match = FULL_FN_ARN_PATTERN.search(lambda_url_config.function_arn).groupdict()\n\n        result = self.lambda_service.invoke(\n            function_name=match.get(\"function_name\"),\n            qualifier=match.get(\"qualifier\"),\n            account_id=match.get(\"account_id\"),\n            region=match.get(\"region_name\"),\n            invocation_type=InvocationType.RequestResponse,\n            client_context=\"{}\",  # TODO: test\n            payload=to_bytes(json.dumps(event)),\n            request_id=gen_amzn_requestid(),\n        )\n        if result.is_error:\n            response = HttpResponse(\"Internal Server Error\", HTTPStatus.BAD_GATEWAY)\n        else:\n            response = lambda_result_to_response(result)\n        return response\n\n\ndef event_for_lambda_url(api_id: str, path: str, data, headers, method: str) -> dict:\n    raw_path = path.split(\"?\")[0]\n    raw_query_string = path.split(\"?\")[1] if len(path.split(\"?\")) > 1 else \"\"\n    query_string_parameters = (\n        {} if not raw_query_string else dict(urllib.parse.parse_qsl(raw_query_string))\n    )\n\n    now = datetime.utcnow()\n    readable = timestamp(time=now, format=TIMESTAMP_READABLE_FORMAT)\n    if not any(char in readable for char in [\"+\", \"-\"]):\n        readable += \"+0000\"\n\n    source_ip = headers.get(\"Remote-Addr\", \"\")\n    request_context = {\n        \"accountId\": \"anonymous\",\n        \"apiId\": api_id,\n        \"domainName\": headers.get(\"Host\", \"\"),\n        \"domainPrefix\": api_id,\n        \"http\": {\n            \"method\": method,\n            \"path\": raw_path,\n            \"protocol\": \"HTTP/1.1\",\n            \"sourceIp\": source_ip,\n            \"userAgent\": headers.get(\"User-Agent\", \"\"),\n        },\n        \"requestId\": long_uid(),\n        \"routeKey\": \"$default\",\n        \"stage\": \"$default\",\n        \"time\": readable,\n        \"timeEpoch\": mktime(ts=now, millis=True),\n    }\n\n    content_type = headers.get(\"Content-Type\", \"\").lower()\n    content_type_is_text = any(text_type in content_type for text_type in [\"text\", \"json\", \"xml\"])\n\n    is_base64_encoded = not (data.isascii() and content_type_is_text) if data else False\n    body = base64.b64encode(data).decode() if is_base64_encoded else data\n    if isinstance(body, bytes):\n        body = to_str(body)\n\n    ignored_headers = [\"connection\", \"x-localstack-tgt-api\", \"x-localstack-request-url\"]\n    event_headers = {k.lower(): v for k, v in headers.items() if k.lower() not in ignored_headers}\n\n    event_headers.update(\n        {\n            \"x-amzn-tls-cipher-suite\": \"ECDHE-RSA-AES128-GCM-SHA256\",\n            \"x-amzn-tls-version\": \"TLSv1.2\",\n            \"x-forwarded-proto\": \"http\",\n            \"x-forwarded-for\": source_ip,\n            \"x-forwarded-port\": str(localstack_host().port),\n        }\n    )\n\n    event = {\n        \"version\": \"2.0\",\n        \"routeKey\": \"$default\",\n        \"rawPath\": raw_path,\n        \"rawQueryString\": raw_query_string,\n        \"headers\": event_headers,\n        \"queryStringParameters\": query_string_parameters,\n        \"requestContext\": request_context,\n        \"body\": body,\n        \"isBase64Encoded\": is_base64_encoded,\n    }\n\n    if not data:\n        event.pop(\"body\")\n\n    return event\n\n\ndef lambda_result_to_response(result: InvocationResult):\n    response = HttpResponse()\n\n    # Set default headers\n    response.headers.update(\n        {\n            \"Content-Type\": \"application/json\",\n            \"Connection\": \"keep-alive\",\n            \"x-amzn-requestid\": result.request_id,\n            \"x-amzn-trace-id\": long_uid(),  # TODO: get the proper trace id here\n        }\n    )\n\n    original_payload = to_str(result.payload)\n    parsed_result = json.loads(original_payload)\n\n    # patch to fix whitespaces\n    # TODO: check if this is a downstream issue of invocation result serialization\n    original_payload = json.dumps(parsed_result, separators=(\",\", \":\"))\n\n    if isinstance(parsed_result, str):\n        # a string is a special case here and is returned as-is\n        response.data = parsed_result\n    elif isinstance(parsed_result, dict):\n        # if it's a dict it might be a proper response\n        if isinstance(parsed_result.get(\"headers\"), dict):\n            response.headers.update(parsed_result.get(\"headers\"))\n        if \"body\" not in parsed_result:\n            # TODO: test if providing a status code but no body actually works\n            response.data = original_payload\n        elif isinstance(parsed_result.get(\"body\"), dict):\n            response.data = json.dumps(parsed_result.get(\"body\"))\n        elif parsed_result.get(\"isBase64Encoded\", False):\n            body_bytes = to_bytes(to_str(parsed_result.get(\"body\", \"\")))\n            decoded_body_bytes = base64.b64decode(body_bytes)\n            response.data = decoded_body_bytes\n        else:\n            response.data = parsed_result.get(\"body\")\n    else:\n        response.data = original_payload\n\n    return response\n", "path": "localstack/services/lambda_/urlrouter.py"}], "after_files": [{"content": "\"\"\"Routing for Lambda function URLs: https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html\"\"\"\nimport base64\nimport json\nimport logging\nimport urllib\nfrom datetime import datetime\nfrom http import HTTPStatus\n\nfrom localstack.aws.api import HttpResponse\nfrom localstack.aws.api.lambda_ import InvocationType\nfrom localstack.aws.protocol.serializer import gen_amzn_requestid\nfrom localstack.http import Request, Router\nfrom localstack.http.dispatcher import Handler\nfrom localstack.services.lambda_.api_utils import FULL_FN_ARN_PATTERN\nfrom localstack.services.lambda_.invocation.lambda_models import InvocationResult\nfrom localstack.services.lambda_.invocation.lambda_service import LambdaService\nfrom localstack.services.lambda_.invocation.models import lambda_stores\nfrom localstack.utils.aws.request_context import AWS_REGION_REGEX\nfrom localstack.utils.strings import long_uid, to_bytes, to_str\nfrom localstack.utils.time import TIMESTAMP_READABLE_FORMAT, mktime, timestamp\nfrom localstack.utils.urls import localstack_host\n\nLOG = logging.getLogger(__name__)\n\n\nclass FunctionUrlRouter:\n    router: Router[Handler]\n    lambda_service: LambdaService\n\n    def __init__(self, router: Router[Handler], lambda_service: LambdaService):\n        self.router = router\n        self.registered = False\n        self.lambda_service = lambda_service\n\n    def register_routes(self) -> None:\n        if self.registered:\n            LOG.debug(\"Skipped Lambda URL route registration (routes already registered).\")\n            return\n        self.registered = True\n\n        LOG.debug(\"Registering parameterized Lambda routes.\")\n\n        self.router.add(\n            \"/\",\n            host=f\"<api_id>.lambda-url.<regex('{AWS_REGION_REGEX}'):region>.<regex('.*'):server>\",\n            endpoint=self.handle_lambda_url_invocation,\n            defaults={\"path\": \"\"},\n        )\n        self.router.add(\n            \"/<path:path>\",\n            host=f\"<api_id>.lambda-url.<regex('{AWS_REGION_REGEX}'):region>.<regex('.*'):server>\",\n            endpoint=self.handle_lambda_url_invocation,\n        )\n\n    def handle_lambda_url_invocation(\n        self, request: Request, api_id: str, region: str, **url_params: dict[str, str]\n    ) -> HttpResponse:\n        response = HttpResponse(headers={\"Content-type\": \"application/json\"})\n\n        lambda_url_config = None\n        try:\n            for account_id in lambda_stores.keys():\n                store = lambda_stores[account_id][region]\n                for fn in store.functions.values():\n                    for url_config in fn.function_url_configs.values():\n                        if url_config.url_id == api_id:\n                            lambda_url_config = url_config\n        except IndexError as e:\n            LOG.warning(f\"Lambda URL ({api_id}) not found: {e}\")\n            response.set_json({\"Message\": None})\n            response.status = \"404\"\n            return response\n\n        event = event_for_lambda_url(\n            api_id, request.full_path, request.data, request.headers, request.method\n        )\n\n        match = FULL_FN_ARN_PATTERN.search(lambda_url_config.function_arn).groupdict()\n\n        result = self.lambda_service.invoke(\n            function_name=match.get(\"function_name\"),\n            qualifier=match.get(\"qualifier\"),\n            account_id=match.get(\"account_id\"),\n            region=match.get(\"region_name\"),\n            invocation_type=InvocationType.RequestResponse,\n            client_context=\"{}\",  # TODO: test\n            payload=to_bytes(json.dumps(event)),\n            request_id=gen_amzn_requestid(),\n        )\n        if result.is_error:\n            response = HttpResponse(\"Internal Server Error\", HTTPStatus.BAD_GATEWAY)\n        else:\n            response = lambda_result_to_response(result)\n        return response\n\n\ndef event_for_lambda_url(api_id: str, path: str, data, headers, method: str) -> dict:\n    raw_path = path.split(\"?\")[0]\n    raw_query_string = path.split(\"?\")[1] if len(path.split(\"?\")) > 1 else \"\"\n    query_string_parameters = (\n        {} if not raw_query_string else dict(urllib.parse.parse_qsl(raw_query_string))\n    )\n\n    now = datetime.utcnow()\n    readable = timestamp(time=now, format=TIMESTAMP_READABLE_FORMAT)\n    if not any(char in readable for char in [\"+\", \"-\"]):\n        readable += \"+0000\"\n\n    source_ip = headers.get(\"Remote-Addr\", \"\")\n    request_context = {\n        \"accountId\": \"anonymous\",\n        \"apiId\": api_id,\n        \"domainName\": headers.get(\"Host\", \"\"),\n        \"domainPrefix\": api_id,\n        \"http\": {\n            \"method\": method,\n            \"path\": raw_path,\n            \"protocol\": \"HTTP/1.1\",\n            \"sourceIp\": source_ip,\n            \"userAgent\": headers.get(\"User-Agent\", \"\"),\n        },\n        \"requestId\": long_uid(),\n        \"routeKey\": \"$default\",\n        \"stage\": \"$default\",\n        \"time\": readable,\n        \"timeEpoch\": mktime(ts=now, millis=True),\n    }\n\n    content_type = headers.get(\"Content-Type\", \"\").lower()\n    content_type_is_text = any(text_type in content_type for text_type in [\"text\", \"json\", \"xml\"])\n\n    is_base64_encoded = not (data.isascii() and content_type_is_text) if data else False\n    body = base64.b64encode(data).decode() if is_base64_encoded else data\n    if isinstance(body, bytes):\n        body = to_str(body)\n\n    ignored_headers = [\"connection\", \"x-localstack-tgt-api\", \"x-localstack-request-url\"]\n    event_headers = {k.lower(): v for k, v in headers.items() if k.lower() not in ignored_headers}\n\n    event_headers.update(\n        {\n            \"x-amzn-tls-cipher-suite\": \"ECDHE-RSA-AES128-GCM-SHA256\",\n            \"x-amzn-tls-version\": \"TLSv1.2\",\n            \"x-forwarded-proto\": \"http\",\n            \"x-forwarded-for\": source_ip,\n            \"x-forwarded-port\": str(localstack_host().port),\n        }\n    )\n\n    event = {\n        \"version\": \"2.0\",\n        \"routeKey\": \"$default\",\n        \"rawPath\": raw_path,\n        \"rawQueryString\": raw_query_string,\n        \"headers\": event_headers,\n        \"queryStringParameters\": query_string_parameters,\n        \"requestContext\": request_context,\n        \"body\": body,\n        \"isBase64Encoded\": is_base64_encoded,\n    }\n\n    if not data:\n        event.pop(\"body\")\n\n    return event\n\n\ndef lambda_result_to_response(result: InvocationResult):\n    response = HttpResponse()\n\n    # Set default headers\n    response.headers.update(\n        {\n            \"Content-Type\": \"application/json\",\n            \"Connection\": \"keep-alive\",\n            \"x-amzn-requestid\": result.request_id,\n            \"x-amzn-trace-id\": long_uid(),  # TODO: get the proper trace id here\n        }\n    )\n\n    original_payload = to_str(result.payload)\n    parsed_result = json.loads(original_payload)\n\n    # patch to fix whitespaces\n    # TODO: check if this is a downstream issue of invocation result serialization\n    original_payload = json.dumps(parsed_result, separators=(\",\", \":\"))\n\n    if isinstance(parsed_result, str):\n        # a string is a special case here and is returned as-is\n        response.data = parsed_result\n    elif isinstance(parsed_result, dict):\n        # if it's a dict it might be a proper response\n        if isinstance(parsed_result.get(\"headers\"), dict):\n            response.headers.update(parsed_result.get(\"headers\"))\n        if \"statusCode\" in parsed_result:\n            response.status_code = int(parsed_result[\"statusCode\"])\n        if \"body\" not in parsed_result:\n            # TODO: test if providing a status code but no body actually works\n            response.data = original_payload\n        elif isinstance(parsed_result.get(\"body\"), dict):\n            response.data = json.dumps(parsed_result.get(\"body\"))\n        elif parsed_result.get(\"isBase64Encoded\", False):\n            body_bytes = to_bytes(to_str(parsed_result.get(\"body\", \"\")))\n            decoded_body_bytes = base64.b64decode(body_bytes)\n            response.data = decoded_body_bytes\n        else:\n            response.data = parsed_result.get(\"body\")\n    else:\n        response.data = original_payload\n\n    return response\n", "path": "localstack/services/lambda_/urlrouter.py"}]}