Skip to content

Quarkus AWS Lambda API Gateway v2 - Multiple Set-Cookie Headers Not Handled Correctly #50075

@juliusz-cwiakalski

Description

@juliusz-cwiakalski

Describe the bug

When using Quarkus as an AWS Lambda handler behind API Gateway v2, multiple Set-Cookie headers are incorrectly merged into a single header in the Lambda response. This causes browsers to fail to set multiple cookies as expected, breaking applications that rely on more than one cookie.

Expected behavior

The response should use the cookies array field as per API Gateway v2 Lambda integration, with each cookie as a separate entry:

{
  "statusCode": 200,
  "headers": {
    "content-length": "40",
    "Content-Type": "application/json"
  },
  "multiValueHeaders": null,
  "cookies": [
    "cookie1=value1;Version=1;Path=/;Max-Age=3600;Secure;HttpOnly;SameSite=Lax",
    "cookie2=value2;Version=1;Path=/;Max-Age=3600;Secure;HttpOnly;SameSite=Lax"
  ],
  "body": "{\"status\":\"ok\",\"rootPath\":\"Hello hello\"}",
  "isBase64Encoded": false
}

With this format, browsers correctly set multiple cookies.

Actual behavior

The response from PingController merges all cookies into a single Set-Cookie header:

{
  "statusCode": 200,
  "headers": {
    "content-length": "40",
    "Set-Cookie": "cookie1=value1;Version=1;Path=/;Max-Age=3600;Secure;HttpOnly;SameSite=Lax,cookie2=value2;Version=1;Path=/;Max-Age=3600;Secure;HttpOnly;SameSite=Lax",
    "Content-Type": "application/json"
  },
  "multiValueHeaders": null,
  "cookies": null,
  "body": "{\"status\":\"ok\",\"rootPath\":\"Hello hello\"}",
  "isBase64Encoded": false
}

As a result, when deployed to AWS, browsers do not set multiple cookies correctly if more than one is present.

How to Reproduce?

Detailed description and steps in project https://github.com/juliusz-cwiakalski/example-bug-quarkus-aws-lambda-gateway-v2-multiple-cookies-broken

In short:

  1. clone the project
  2. ./gradlew build -Dquarkus.package.jar.enabled=true -Dquarkus.package.jar.type=legacy-jar -Dquarkus.native.enabled=false
  3. sam local invoke --template ./build/sam.jvm.yaml -e ./example-gateway-event.json

Output of uname -a or ver

Linux juliusz-Latitude-5421 6.8.0-79-generic #79-Ubuntu SMP PREEMPT_DYNAMIC Tue Aug 12 14:42:46 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

Output of java -version

openjdk version "21.0.5" 2024-10-15 LTS OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS) OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing)

Quarkus version or git rev

3.24.4

Build tool (ie. output of mvnw --version or gradlew --version)

------------------------------------------------------------ Gradle 8.14 ------------------------------------------------------------ Build time: 2025-04-25 09:29:08 UTC Revision: 34c560e3be961658a6fbcd7170ec2443a228b109 Kotlin: 2.0.21 Groovy: 3.0.24 Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024 Launcher JVM: 21.0.5 (Amazon.com Inc. 21.0.5+11-LTS) Daemon JVM: /home/juliusz/.sdkman/candidates/java/21.0.5-amzn (no JDK specified, using current Java home) OS: Linux 6.8.0-79-generic amd64

Additional information

Root Cause Analysis

The bug is caused by the following logic in LambdaHttpHandler:

// Handle cookies separately to preserve commas in the header values
@Override
public void handleMessage(Object msg) {
    try {
        // ...existing code...
        if (msg instanceof HttpResponse res) {
            responseBuilder.setStatusCode(res.status().code());
            final Map<String, String> headers = new HashMap<>();
            responseBuilder.setHeaders(headers);
            for (String name : res.headers().names()) {
                final List<String> allForName = res.headers().getAll(name);
                if (allForName == null || allForName.isEmpty()) {
                    continue;
                }
                // Handle cookies separately to preserve commas in the header values
                if ("set-cookie".equals(name)) {
                    responseBuilder.setCookies(allForName);
                    continue;
                }
                // ...
            }
        }
    }
}

However, in the Lambda runtime context, the msg is of type io.vertx.core.http.impl.AssembledFullHttpResponse, and its headers are an instance of io.vertx.core.http.impl.headers.HeadersMultiMap, which is case-insensitive and converts all header names to Camel-Kebab-Case. Thus, the check for "set-cookie" fails because the actual header name is "Set-Cookie".

Solution

Change the header name check to be case-insensitive:

if ("set-cookie".equalsIgnoreCase(name)) {
    responseBuilder.setCookies(allForName);
    continue;
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    3.27.1

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions