A regression appears when running a Spring Boot 3.5.5 (Spring Framework 6.2.9 or later) application on AWS Lambda behind an Application Load Balancer. Requests that should produce HTTP 400 (Jakarta Bean Validation failure on a @Size(max=1) field) return HTTP 502 only when invoked through the ALB. The same Lambda invoked via its Function URL correctly returns HTTP 400. Rolling back to Spring 6.2.8 restores correct 400 behavior through the ALB.

Environment: Java 21 Lambda Spring Boot 3.5.5 Spring Framework 6.2.9+ (regression); 6.2.8 works Dependency: aws-serverless-java-container-springboot3:2.1.4 ALB → Lambda target group (multi-value headers enabled) Expected: Invalid payload ({"name":"aa"}) → HTTP 400 with validation message. Actual (6.2.9+ via ALB): HTTP 502 (no body). Direct Lambda URL: Always returns 400 (all versions).

Reproduction steps (simplified): Deploy stack (Lambda + ALB) with Spring 6.2.9+. PUT /audit/v1/event-registrations/hello body: {"name":"aa"} via ALB → 502. Same request via Function URL → 400. Change Spring to 6.2.8 in build.gradle.kts, redeploy → ALB now returns 400. Impact: ALB path unusable for clients relying on validation error semantics.

The ALB path through to the lambda application is unusable for clients relying on validation for springboot v3.5.5 using spring framework v6.2.10 affecting our service and security upgrades.

alb-springboot-lambda-gateway-issue.zip

Comment From: sdeleuze

I think we would need a more minimal repro usable locally without AWS stuff and/or an analysis of what triggers the difference of behavior between Direct Lambda URL and ALB to have something actionable on our side. Like what are the differences between with and without ALB in terms of HTTP headers, etc.

Comment From: jimmytang-connectid

Hi Sébastien,

It looks like you were looking for an analysis of what triggers the difference of behaviour between Direct Lambda URL and ALB.

The zip file containing source code, cdk infra code to reproduce the issue on a deployed environment, and verbose curl outputs that show the difference between the http headers with and without the alb.

alb-springboot-lambda-gateway-issue.zip

Below there are 2 code blocks that show the verbose curl outputs first through the ALB demonstrating the issue, and then second directly to the lambda URL which contains the request and response headers as well as the response body, and the curl command used with a place holder for the domain.

v6.2.10 through the ALB (testcase-2.txt)

curl -v -H "Content-Type: application/json" -X PUT -d '{"name": "aa"}' <ALB_ENDPOINT>/audit/v1/event-registrations/hello
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Host AuditL-Audit-DLwRC2NVFw51-1108827923.ap-southeast-2.elb.amazonaws.com:80 was resolved.
* IPv6: 2405:6e00:64::368:3d28, 2405:6e00:64::343f:1d1b
* IPv4: 52.63.29.27, 3.104.61.40
*   Trying [2405:6e00:64::368:3d28]:80...
* Connected to AuditL-Audit-DLwRC2NVFw51-1108827923.ap-southeast-2.elb.amazonaws.com (2405:6e00:64::368:3d28) port 80
> PUT /audit/v1/event-registrations/hello HTTP/1.1
> Host: AuditL-Audit-DLwRC2NVFw51-1108827923.ap-southeast-2.elb.amazonaws.com
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 14
> 
} [14 bytes data]
* upload completely sent off: 14 bytes
< HTTP/1.1 502 Bad Gateway
< Server: awselb/2.0
< Date: Wed, 10 Sep 2025 05:24:51 GMT
< Content-Type: text/html
< Connection: keep-alive
< Content-Length: 122
< 
{ [122 bytes data]

100   136  100   122  100    14    750     86 --:--:-- --:--:-- --:--:--   839
* Connection #0 to host AuditL-Audit-DLwRC2NVFw51-1108827923.ap-southeast-2.elb.amazonaws.com left intact
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>

v6.2.10 Directly to the Lambda URL (testcase-3.txt)

curl -v -H "Content-Type: application/json" -X PUT -d '{"name": "aa"}' <FUNCTION_URL_ENDPOINT>/audit/v1/event-registrations/hello
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Host mkmnac5kyzr5eraz3mbddlhzau0gbppm.lambda-url.ap-southeast-2.on.aws:443 was resolved.
* IPv6: 2406:da1c:7bd:9601:8bec:1fc6:a0ce:a845, 2406:da1c:7bd:9603:423d:ee8b:562a:7bf0, 2406:da1c:7bd:9602:a6cc:9c27:b9c2:9657
* IPv4: 16.176.105.73, 54.252.66.252, 54.253.174.220
*   Trying [2406:da1c:7bd:9601:8bec:1fc6:a0ce:a845]:443...
* Connected to mkmnac5kyzr5eraz3mbddlhzau0gbppm.lambda-url.ap-southeast-2.on.aws (2406:da1c:7bd:9601:8bec:1fc6:a0ce:a845) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
} [370 bytes data]
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* (304) (IN), TLS handshake, Unknown (8):
{ [6 bytes data]
* (304) (IN), TLS handshake, Certificate (11):
{ [3208 bytes data]
* (304) (IN), TLS handshake, CERT verify (15):
{ [79 bytes data]
* (304) (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* (304) (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384 / [blank] / UNDEF
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
*  subject: CN=*.lambda-url.ap-southeast-2.on.aws
*  start date: Aug 11 05:24:54 2025 GMT
*  expire date: Sep 10 05:24:54 2026 GMT
*  subjectAltName: host "mkmnac5kyzr5eraz3mbddlhzau0gbppm.lambda-url.ap-southeast-2.on.aws" matched cert's "*.lambda-url.ap-southeast-2.on.aws"
*  issuer: emailAddress=certadmin@netskope.com; CN=ca.auspayplus.au.goskope.com; OU=55d9b8da69560e9d21dd4a2cbab35297; O=Eftpos Payments; L=Sydney; ST=NSW; C=AU
*  SSL certificate verify ok.
* using HTTP/1.x

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0> PUT /audit/v1/event-registrations/hello HTTP/1.1
> Host: mkmnac5kyzr5eraz3mbddlhzau0gbppm.lambda-url.ap-southeast-2.on.aws
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 14
> 
} [14 bytes data]
* upload completely sent off: 14 bytes
< HTTP/1.1 400 Bad Request
< Date: Wed, 10 Sep 2025 05:24:54 GMT
< Content-Type: application/json
< Connection: keep-alive
< x-amzn-RequestId: 0e1a2cdd-4b56-4dd5-b057-5eb945487aaa
< X-Amzn-Trace-Id: Root=1-68c10ba6-25630691731316fe3cd73921;Parent=0c67e8a53f5bf60d;Sampled=0;Lineage=1:899ee7f3:0
< Content-Length: 42
< 
{ [42 bytes data]

100    56  100    42  100    14     53     17 --:--:-- --:--:-- --:--:--    71
* Connection #0 to host mkmnac5kyzr5eraz3mbddlhzau0gbppm.lambda-url.ap-southeast-2.on.aws left intact
{"message":"size must be between 0 and 1"}

Comment From: sdeleuze

The zip file containing source code, cdk infra code to reproduce the issue on a deployed environment

We don't have access to an AWS environment, does your reproducer enable us to reproduce the issue locally without any AWS account?

I would also be interested to know if you see Spring related logs in the ALB use case that indicates that the request reaches the Spring Boot application when you get the 502 response.

Comment From: erikpragt-connectid

Hi @sdeleuze , after having contact with AWS support, I think we found the issue. The issue is most likely introduced in this commit: https://github.com/spring-projects/spring-framework/commit/6bd12e8680d5e604d55de5e809548e4145ba0734

This some of the data from our logs:

The JSON response being returned to ALB by the Lambda function had an incorrect body parameter.
Error :- "header item should contain JSON string object only [Content-Disposition"

2025/09/12 02:49:01.120 [error] [LambdaMessageParserMultiValue]: header array should contain json string object only [Content-Disposition]

2025/09/12 02:49:01.120 [error] [LambdaMessageParser]: [deserialize_to_http_response] failed to deserialize headers token, json parsing error. ret = [3].

In the failing version of Spring (Spring 6.2.10), the response we see in our test is this:

{"statusCode":400,"statusDescription":"400 Bad Request","multiValueHeaders":{"Content-Disposition":[null]},"body":"","isBase64Encoded":false}

Note the null value for the Content-Disposition header. Even if we hardcode the field in our response, it's getting ignored.

In the working version of Spring (Srping 6.2.8), the response looks like this:

{"statusCode":400,"statusDescription":"400 Bad Request","multiValueHeaders":{},"body":"","isBase64Encoded":false}

This is most likely caused by the change in the DispatcherServlet here:

response.setHeader(HttpHeaders.CONTENT_TYPE, null);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, null);

I'm not 100% sure why the CONTENT_TYPE header doesn't show up in the response, but I did see where was some special handling for CONTENT_TYPE vs other headers, but I see this is in the aws-serverless-java-container project:

See AwsHttpServletResponse:

    @Override
    public void setHeader(String s, String s1) {
        if (!canSetHeader()) return;
        if (isContentTypeHeader(s)) {
            setContentType(s1);
        } else {
            setHeader(s, s1, true);
        }

My guess here is that the introduction of the new header broke the AWS Lambda integration. Would this line of thinking be correct?

Comment From: bclozel

I think this is a bug in the AWS Servlet library. Setting a null value for a response header should delete it, not add "null" as an entry.

This is clearly documented in the official specification and this is the behavior we are seeing for compliant servers.

Comment From: bclozel

Closing in favor of https://github.com/aws/serverless-java-container/issues/1522