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-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; 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