Repository: awslabs/aws-lambda-rust-runtime (language: rust)

Issue:
lambda_http 0.11.3 breaks response headers
I'm getting no response headers when using the latest version of lambda http,

I am using this config in cargo.toml
`lambda_http = { version = "=0.11.3",default-features = false, features = ["apigw_http"] }`

0.11.1 does not have this issue:

Here is how I set the response in the handler

```
   Ok(Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body("something".into())
        .map_err(Box::new)?)
```
curl -v shows this as the headers with version 0.11.1
```
< HTTP/2 200 
< date: Wed, 15 May 2024 13:48:35 GMT
< content-type: text/html
< content-length: 553
< apigw-requestid: ...
```

and for 0.11.3:
```
< HTTP/2 200 
< date: Wed, 15 May 2024 13:52:58 GMT
< content-type: text/plain; charset=utf-8
< content-length: 553
< apigw-requestid: ...
```

This seems related to this commit
[fc49dd5](https://github.com/awslabs/aws-lambda-rust-runtime/commit/fc49dd5e0f1dd0c81bdb51c6bfcfb755583ba7ff)
which is the only one in 0.11.3



Discussion / hints:
This also broke us, it broke CORS Allow-Origin headers in our case.
Ugh. I'm very sorry about that. I've yanked that version from crates.io. It looks like this is not consistent in all the integrations: https://github.com/awslabs/aws-lambda-rust-runtime/pull/852. We'll investigate further.
@jantb how did you create your APIGW? I wonder if there are some settings that we're missing, and affect how headers are returned
@shortjared which service do you integrate with, APIGW? which kind? If you have any configuration that you can share, it'd be very helpful.
Here is the setup of the http APIGW: 

``` kotlin
val httpApi =
            HttpApi(
                this, "Api", HttpApiProps.builder()
                    .apiName("api")
                    .corsPreflight(
                        CorsPreflightOptions.builder().allowOrigins(
                            listOf(
                                "xxx",
                                "xxx",
                                "http://localhost:8080",
                            )
                        ).allowMethods(CorsHttpMethod.entries)
                            .build()
                    )
                    .build()
            )
```
  and the functions are added with:
  
``` kotlin
  AddRoutesOptions.builder().path(path).methods(listOf(method))
            .integration(
                HttpLambdaIntegration(
                    "$functionName-integration",
                    function
                )
            ).build()     
```     
Cloudformation:
``` json
{"Api": {
   "Type": "AWS::ApiGatewayV2::Api",
   "Properties": {
    "CorsConfiguration": {
     "AllowMethods": [
      "*",
      "DELETE",
      "GET",
      "HEAD",
      "OPTIONS",
      "PATCH",
      "POST",
      "PUT"
     ],
     "AllowOrigins": [
      "",
      "",
      "http://localhost:8080"
     ]
    },
    "Name": "api",
    "ProtocolType": "HTTP"
   },
   "Metadata": {
    "aws:cdk:path": "xxx/Api/Resource"
   }
  }
  }
```

```json
{
  "ApiGETservehtmlintegration": {
   "Type": "AWS::ApiGatewayV2::Integration",
   "Properties": {
    "ApiId": {
     "Ref": "ApiF"
    },
    "IntegrationType": "AWS_PROXY",
    "IntegrationUri": {
     "Fn::GetAtt": [
      "servehtml",
      "Arn"
     ]
    },
    "PayloadFormatVersion": "2.0"
   },
   "Metadata": {
    "aws:cdk:path": "xxxx/Api/GET--/serve-html-integration/Resource"
   }
  }}
            
Also reporting that lambda function URL regression (previously in #871) is still present in 0.11.3.

Lambda function URLs use API gateway v2 format:

>The request and response event formats follow the same schema as the [Amazon API Gateway payload format version 2.0](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format).

https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads

---

Note that in #852, the author references a pull request from `aws-lambda-go-api-proxy` repo from 2020. That was before the code base was refactored to split the types for API gateway v1/v2 & ALB integrations.

- https://github.com/awslabs/aws-lambda-go-api-proxy/blob/3f6c8160ae0c22b0bd05b2e3a9122736f035c74b/core/response.go#L117
- https://github.com/awslabs/aws-lambda-go-api-proxy/blob/3f6c8160ae0c22b0bd05b2e3a9122736f035c74b/core/responseALB.go#L108
- https://github.com/awslabs/aws-lambda-go-api-proxy/blob/3f6c8160ae0c22b0bd05b2e3a9122736f035c74b/core/responsev2.go#L117

They currently handle header keys differently based on integration types there on `master`.

Fix patch:
diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs
index c1b48ef2..6a31cb79 100644
--- a/lambda-http/src/response.rs
+++ b/lambda-http/src/response.rs
@@ -69,8 +69,8 @@ impl LambdaResponse {
                 body,
                 is_base64_encoded,
                 status_code: status_code as i64,
-                // explicitly empty, as API gateway does not properly merge headers and
-                // multi-value-headers, resulting in duplicate headers
+                // Explicitly empty, as API gateway v1 will merge "headers" and
+                // "multi_value_headers" fields together resulting in duplicate response headers.
                 headers: HeaderMap::new(),
                 multi_value_headers: headers,
             }),
@@ -93,10 +93,10 @@ impl LambdaResponse {
                     is_base64_encoded,
                     status_code: status_code as i64,
                     cookies,
-                    // explicitly empty, as API gateway does not properly merge headers and
-                    // multi-value-headers, resulting in duplicate headers
-                    headers: HeaderMap::new(),
-                    multi_value_headers: headers,
+                    // API gateway v2 doesn't have "multi_value_headers" field. Duplicate headers
+                    // are combined with commas and included in the headers field.
+                    headers,
+                    multi_value_headers: HeaderMap::new(),
                 })
             }
             #[cfg(feature = "alb")]
@@ -104,10 +104,9 @@ impl LambdaResponse {
                 body,
                 status_code: status_code as i64,
                 is_base64_encoded,
-                // ALB responses are used for ALB integrations as well as
-                // Lambda Function URLs. The former uses the `multi_value_headers` field,
-                // while the later uses the `headers` field. We need to return
-                // both fields to ensure both integrations work correctly.
+                // ALB responses are used for ALB integration, which can be configured to use
+                // either "headers" or "multi_value_headers" field. We need to return both fields
+                // to ensure both configuration work correctly.
                 headers: headers.clone(),
                 multi_value_headers: headers,
                 status_description: Some(format!(
@@ -121,8 +120,8 @@ impl LambdaResponse {
                 body,
                 is_base64_encoded,
                 status_code: status_code as i64,
-                // explicitly empty, as API gateway does not properly merge headers and
-                // multi-value-headers, resulting in duplicate headers
+                // Explicitly empty, as API gateway v1 will merge "headers" and
+                // "multi_value_headers" fields together resulting in duplicate response headers.
                 headers: HeaderMap::new(),
                 multi_value_headers: headers,
             }),
@@ -475,7 +474,7 @@ mod tests {
         let json = serde_json::to_string(&response).expect("failed to serialize to json");
         assert_eq!(
             json,
-            r#"{"statusCode":200,"headers":{},"multiValueHeaders":{"content-encoding":["gzip"]},"body":"MDAwMDAw","isBase64Encoded":true,"cookies":[]}"#
+            r#"{"statusCode":200,"headers":{"content-encoding":"gzip"},"multiValueHeaders":{},"body":"MDAwMDAw","isBase64Encoded":true,"cookies":[]}"#
         )
     }
 
@@ -493,7 +492,7 @@ mod tests {
         let json = serde_json::to_string(&response).expect("failed to serialize to json");
         assert_eq!(
             json,
-            r#"{"statusCode":200,"headers":{},"multiValueHeaders":{"content-type":["application/json"]},"body":"000000","isBase64Encoded":false,"cookies":[]}"#
+            r#"{"statusCode":200,"headers":{"content-type":"application/json"},"multiValueHeaders":{},"body":"000000","isBase64Encoded":false,"cookies":[]}"#
         )
     }
 
@@ -511,7 +510,7 @@ mod tests {
         let json = serde_json::to_string(&response).expect("failed to serialize to json");
         assert_eq!(
             json,
-            r#"{"statusCode":200,"headers":{},"multiValueHeaders":{"content-type":["application/json; charset=utf-16"]},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"#
+            r#"{"statusCode":200,"headers":{"content-type":"application/json; charset=utf-16"},"multiValueHeaders":{},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"#
         )
     }
 
@@ -529,7 +528,7 @@ mod tests {
         let json = serde_json::to_string(&response).expect("failed to serialize to json");
         assert_eq!(
             json,
-            r#"{"statusCode":200,"headers":{},"multiValueHeaders":{"content-type":["application/graphql-response+json; charset=utf-16"]},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"#
+            r#"{"statusCode":200,"headers":{"content-type":"application/graphql-response+json; charset=utf-16"},"multiValueHeaders":{},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"#
         )
     }
 


Test patch: