From 211b6594c99bdeb0e012179e3c6ec869fc38b140 Mon Sep 17 00:00:00 2001 From: Chinmay Kawle Date: Wed, 21 Oct 2026 17:57:28 +0000 Subject: [PATCH 1/2] fix: update Jackson to 3.2.4 and Spring to 7.0.9, flush async responses Updates Jackson and Spring to current patch releases, and fixes the async response hang that newer Spring versions cause. Dependency changes: - Jackson 3.0.2 -> 3.2.4. jackson.version moves from core to the parent pom so springboot4 can align on it. - spring module: Spring Framework 7.0.0 -> 7.0.9, Spring Security 7.0.0 -> 7.0.7. The spring archetype gets 7.0.9 in pom.xml and build.gradle. - springboot4: Spring Boot 4.0.0 -> 4.0.8, Spring Framework 7.0.1 -> 7.0.9, Spring Security 7.0.0 -> 7.0.7, Spring Cloud Function 5.0.0 -> 5.0.4. Imports jackson-bom at core's version and pins slf4j-api and jspecify so dependencyConvergence passes. - springboot4 archetype: Boot parent 4.0.8 with jackson-bom.version and jackson-2-bom.version overrides (Jackson 3.2 needs annotations 2.22). - slf4j 2.0.17 -> 2.0.18 to match Boot. Async fix: Since Spring Framework 7.0.6, ServletServerHttpResponse.getBody() returns a stream that ignores flush() unless spring.http.response.flush.enabled is set. We relied on that flush to commit the response after an async re-dispatch, and doFilter skips its own flush for ASYNC requests, so a DeferredResult endpoint never released the response latch. doFilter now flushes after the re-dispatch unless the dispatch target started a new async cycle. AsyncAppTest gets a 30 second timeout so a regression fails instead of hanging the build. --- aws-serverless-java-container-core/pom.xml | 1 - .../AwsLambdaServletContainerHandler.java | 10 +++++++ aws-serverless-java-container-spring/pom.xml | 4 +-- .../serverless/proxy/spring/AsyncAppTest.java | 2 ++ .../pom.xml | 27 ++++++++++++++++--- .../archetype-resources/build.gradle | 4 +-- .../resources/archetype-resources/pom.xml | 2 +- .../resources/archetype-resources/pom.xml | 6 ++++- pom.xml | 4 +-- 9 files changed, 47 insertions(+), 13 deletions(-) diff --git a/aws-serverless-java-container-core/pom.xml b/aws-serverless-java-container-core/pom.xml index 8fbdeebc6..d561495bb 100644 --- a/aws-serverless-java-container-core/pom.xml +++ b/aws-serverless-java-container-core/pom.xml @@ -18,7 +18,6 @@ 3.1.0 6.1.0 - 3.0.2 diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java index 7437449e1..fa789d590 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java @@ -153,8 +153,14 @@ protected void doFilter(HttpServletRequest request, HttpServletResponse response FilterChain chain = getFilterChain(request, servlet); chain.doFilter(request, response); if(requiresAsyncReDispatch(request)) { + AsyncContext dispatchedContext = request.getAsyncContext(); chain = getFilterChain(request, servlet); chain.doFilter(request, response); + // like a servlet container, we finish the response after the async dispatch unless the dispatch + // target started a new async cycle. Spring 7.0.6+ no longer flushes the body stream for us. + if (!response.isCommitted() && !startedNewAsyncContext(request, dispatchedContext)) { + response.flushBuffer(); + } } // if for some reason the response wasn't flushed yet, we force it here unless it's being processed asynchronously (WebFlux) if (!response.isCommitted() && request.getDispatcherType() != DispatcherType.ASYNC) { @@ -162,6 +168,10 @@ protected void doFilter(HttpServletRequest request, HttpServletResponse response } } + private boolean startedNewAsyncContext(HttpServletRequest request, AsyncContext dispatchedContext) { + return request.isAsyncStarted() && request.getAsyncContext() != dispatchedContext; + } + private boolean requiresAsyncReDispatch(HttpServletRequest request) { if (request.isAsyncStarted()) { AsyncContext asyncContext = request.getAsyncContext(); diff --git a/aws-serverless-java-container-spring/pom.xml b/aws-serverless-java-container-spring/pom.xml index 1cafa8660..761923ef0 100644 --- a/aws-serverless-java-container-spring/pom.xml +++ b/aws-serverless-java-container-spring/pom.xml @@ -16,8 +16,8 @@ - 7.0.0 - 7.0.0 + 7.0.9 + 7.0.7 diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/AsyncAppTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/AsyncAppTest.java index 8265e1bf4..09cd88b17 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/AsyncAppTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/AsyncAppTest.java @@ -9,6 +9,7 @@ import com.amazonaws.serverless.proxy.spring.springapp.MessageController; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -36,6 +37,7 @@ void springApp_helloRequest_returnsCorrect() { } @Test + @Timeout(30) void springApp_asyncRequest_returnsCorrect() { AwsProxyRequest req = new AwsProxyRequestBuilder("/async", "GET").build(); AwsProxyResponse resp = handler.handleRequest(req, new MockLambdaContext()); diff --git a/aws-serverless-java-container-springboot4/pom.xml b/aws-serverless-java-container-springboot4/pom.xml index 9c7d90921..4e818d7d1 100644 --- a/aws-serverless-java-container-springboot4/pom.xml +++ b/aws-serverless-java-container-springboot4/pom.xml @@ -15,13 +15,21 @@ https://aws.amazon.com/lambda - 7.0.1 - 4.0.0 - 7.0.0 + 7.0.9 + 4.0.8 + 7.0.7 + + + tools.jackson + jackson-bom + ${jackson.version} + pom + import + org.springframework spring-framework-bom @@ -29,6 +37,17 @@ pom import + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.jspecify + jspecify + 1.0.1 + @@ -37,7 +56,7 @@ org.springframework.cloud spring-cloud-function-serverless-web - 5.0.0 + 5.0.4 com.amazonaws.serverless diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle index 9ba701351..51bdc7024 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/build.gradle @@ -7,8 +7,8 @@ repositories { dependencies { implementation ( - 'org.springframework:spring-webmvc:6.2.8', - 'org.springframework:spring-context:6.2.8', + 'org.springframework:spring-webmvc:7.0.9', + 'org.springframework:spring-context:7.0.9', 'com.amazonaws.serverless:aws-serverless-java-container-spring:[2.0-SNAPSHOT,)', 'org.apache.logging.log4j:log4j-core:2.24.3', 'org.apache.logging.log4j:log4j-api:2.24.3', diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml index 3a03375c8..c49d347cd 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ 1.8 1.8 - 7.0.0 + 7.0.9 6.0.0 2.24.2 diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml index c122a405a..f44d5280b 100644 --- a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,12 +16,16 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0 + 4.0.8 17 5.12.1 + + 3.2.4 + 2.22.4 diff --git a/pom.xml b/pom.xml index 50257e2e3..1b8c4b626 100644 --- a/pom.xml +++ b/pom.xml @@ -78,8 +78,8 @@ 0.7 12.1.1 - - 2.0.17 + 3.2.4 + 2.0.18 6.0.0 5.19.0 1.3 From f7fe55024d9ca09a6f3766aba900ac6a951643a6 Mon Sep 17 00:00:00 2001 From: Chinmay Kawle Date: Wed, 21 Oct 2026 20:32:29 +0000 Subject: [PATCH 2/2] test: cover async re-dispatch flush in AwsLambdaServletContainerHandler Adds two AwsAsyncContextTest cases that run the real doFilter with a plain servlet behaving like Spring MVC 7.0.6+: it starts async and dispatches on the first pass, then writes the body without flushing on the async dispatch. One asserts the container commits the response and releases the latch; the other asserts the response stays open when the dispatch starts a new async cycle. The first fails without the doFilter flush change. --- .../internal/servlet/AwsAsyncContextTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java index a8383b5c3..c65462b5e 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsAsyncContextTest.java @@ -14,16 +14,21 @@ import org.junit.jupiter.api.Test; import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Servlet; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRegistration; +import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class AwsAsyncContextTest { private MockLambdaContext lambdaCtx = new MockLambdaContext(); @@ -46,6 +51,47 @@ void dispatch_amendsPath() throws InvalidRequestEventException { assertEquals("/srv1/hello", req.getRequestURI()); } + @Test + void doFilter_asyncDispatchWritesWithoutFlush_flushesResponse() throws Exception { + PassThroughContainerHandler passThroughHandler = new PassThroughContainerHandler(); + AsyncDispatchServlet servlet = new AsyncDispatchServlet(false); + AwsProxyHttpServletRequest req = asyncDispatchRequest(passThroughHandler, servlet); + CountDownLatch latch = new CountDownLatch(1); + AwsHttpServletResponse resp = new AwsHttpServletResponse(req, latch); + req.setResponse(resp); + + passThroughHandler.doFilter(req, resp, servlet); + + assertTrue(resp.isCommitted()); + assertEquals(0, latch.getCount()); + assertEquals("hello", resp.getAwsResponseBodyString()); + } + + @Test + void doFilter_asyncDispatchStartsNewAsync_leavesResponseOpen() throws Exception { + PassThroughContainerHandler passThroughHandler = new PassThroughContainerHandler(); + AsyncDispatchServlet servlet = new AsyncDispatchServlet(true); + AwsProxyHttpServletRequest req = asyncDispatchRequest(passThroughHandler, servlet); + CountDownLatch latch = new CountDownLatch(1); + AwsHttpServletResponse resp = new AwsHttpServletResponse(req, latch); + req.setResponse(resp); + + passThroughHandler.doFilter(req, resp, servlet); + + assertFalse(resp.isCommitted()); + assertEquals(1, latch.getCount()); + } + + private AwsProxyHttpServletRequest asyncDispatchRequest(PassThroughContainerHandler passThroughHandler, Servlet servlet) throws InvalidRequestEventException { + AwsServletContext servletCtx = new AwsServletContext(passThroughHandler); + passThroughHandler.setServletContext(servletCtx); + servletCtx.addServlet("async", servlet).addMapping("/async"); + + AwsProxyHttpServletRequest req = (AwsProxyHttpServletRequest)reader.readRequest(new AwsProxyRequestBuilder("/async", "GET").build(), null, lambdaCtx, LambdaContainerHandler.getContainerConfig()); + req.setServletContext(servletCtx); + return req; + } + private AwsServletContext getCtx() { AwsServletContext ctx = new AwsServletContext(handler); @@ -59,6 +105,54 @@ private AwsServletContext getCtx() { return ctx; } + /** + * Behaves like Spring MVC 7.0.6+ with a result that's ready right away: starts async and dispatches during the + * first pass, then on the async dispatch writes the body without flushing it. + */ + private static class AsyncDispatchServlet extends HttpServlet { + private final boolean startAsyncOnDispatch; + + AsyncDispatchServlet(boolean startAsyncOnDispatch) { + this.startAsyncOnDispatch = startAsyncOnDispatch; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + if (req.getDispatcherType() != DispatcherType.ASYNC) { + req.startAsync().dispatch(); + } else if (startAsyncOnDispatch) { + req.startAsync(); + } else { + resp.setStatus(200); + resp.getOutputStream().write("hello".getBytes(StandardCharsets.UTF_8)); + } + } + } + + /** + * Uses the real doFilter from AwsLambdaServletContainerHandler, unlike MockContainerHandler. + */ + public static class PassThroughContainerHandler extends AwsLambdaServletContainerHandler { + public PassThroughContainerHandler() { + super(AwsProxyRequest.class, AwsProxyResponse.class, new AwsProxyHttpServletRequestReader(), new AwsProxyHttpServletResponseWriter(), new AwsProxySecurityContextWriter(), new AwsProxyExceptionHandler()); + } + + @Override + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { + return new AwsHttpServletResponse(request, latch); + } + + @Override + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + + } + + @Override + public void initialize() throws ContainerInitializationException { + + } + } + public static class MockContainerHandler extends AwsLambdaServletContainerHandler { private int desiredStatus; private HttpServletResponse response;