Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion aws-serverless-java-container-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<properties>
<jaxrs.version>3.1.0</jaxrs.version>
<servlet.version>6.1.0</servlet.version>
<jackson.version>3.0.2</jackson.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,25 @@ 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) {
response.flushBuffer();
}
}

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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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<AwsProxyRequest, AwsProxyResponse, HttpServletRequest, AwsHttpServletResponse> {
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<AwsProxyRequest, AwsProxyResponse, HttpServletRequest, AwsHttpServletResponse> {
private int desiredStatus;
private HttpServletResponse response;
Expand Down
4 changes: 2 additions & 2 deletions aws-serverless-java-container-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
</parent>

<properties>
<spring.version>7.0.0</spring.version>
<spring-security.version>7.0.0</spring-security.version>
<spring.version>7.0.9</spring.version>
<spring-security.version>7.0.7</spring-security.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a test with a servlet that starts async and dispatches, asserting the response gets flushed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. Thanks!

Expand Down Expand Up @@ -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());
Expand Down
27 changes: 23 additions & 4 deletions aws-serverless-java-container-springboot4/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,39 @@
<url>https://aws.amazon.com/lambda</url>

<properties>
<spring.version>7.0.1</spring.version>
<springboot.version>4.0.0</springboot.version>
<springsecurity.version>7.0.0</springsecurity.version>
<spring.version>7.0.9</spring.version>
<springboot.version>4.0.8</springboot.version>
<springsecurity.version>7.0.7</springsecurity.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- Spring Boot manages an older Jackson 3 than core, align on core's version -->
<dependency>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Boot's transitive dependencies disagree on these, pin them to the versions Boot manages -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand All @@ -37,7 +56,7 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-serverless-web</artifactId>
<version>5.0.0</version>
<version>5.0.4</version>
</dependency>
<dependency>
<groupId>com.amazonaws.serverless</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>7.0.0</spring.version>
<spring.version>7.0.9</spring.version>
<junit.version>6.0.0</junit.version>
<log4j.version>2.24.2</log4j.version>
</properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
<version>4.0.8</version>
</parent>

<properties>
<java.version>17</java.version>
<junit.version>5.12.1</junit.version>
<!-- Spring Boot 4.0 manages Jackson 3.1.x, use the Jackson version aws-serverless-java-container is built with.
Jackson 3.2 needs jackson-annotations 2.22, which Boot manages through its Jackson 2 BOM. -->
<jackson-bom.version>3.2.4</jackson-bom.version>
<jackson-2-bom.version>2.22.4</jackson-2-bom.version>
</properties>

<dependencies>
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
<properties>
<jacoco.minCoverage>0.7</jacoco.minCoverage>
<dependencyCheck.version>12.1.1</dependencyCheck.version>
<!-- <jackson.version>2.19.1</jackson.version> -->
<slf4j.version>2.0.17</slf4j.version>
<jackson.version>3.2.4</jackson.version>
<slf4j.version>2.0.18</slf4j.version>
<junit.version>6.0.0</junit.version>
<mockito.version>5.19.0</mockito.version>
<hamcrest.version>1.3</hamcrest.version>
Expand Down
Loading