diff --git a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java
index 79d030bda..5f44f89f7 100644
--- a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java
+++ b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyHandlerFilter.java
@@ -52,10 +52,15 @@
import static com.amazonaws.serverless.proxy.RequestReader.LAMBDA_CONTEXT_PROPERTY;
/**
- * Servlet filter class that calls Jersey's ApplicationHandler. Given a Jax RS Application object, this class
- * initializes a Jersey {@link ApplicationHandler} and calls its handle method. Requests are transformed
- * to ContainerRequest objects by the {@link #servletRequestToContainerRequest(ServletRequest)} method.
+ * Calls Jersey's ApplicationHandler. Given a Jax RS Application object, this class initializes a Jersey
+ * {@link ApplicationHandler} and calls its handle method. Requests are transformed to
+ * ContainerRequest objects by the {@link #servletRequestToContainerRequest(ServletRequest)} method.
* Jersey responses are written directly to the ServletResponse by the {@link JerseyServletResponseWriter}.
+ *
+ * {@link JerseyLambdaContainerHandler} invokes this class from the servlet at the end of the filter chain, so that
+ * application filters run before a JAX-RS resource. It is no longer registered as a servlet filter itself. The
+ * Filter interface is still implemented because this class is public, but {@link #doFilter} is
+ * deprecated: Jersey writes the response, so there is nothing for a filter after it to do.
*/
public class JerseyHandlerFilter implements Filter, Container {
public static final String JERSEY_SERVLET_REQUEST_PROPERTY = "com.amazonaws.serverless.jersey.servletRequest";
@@ -86,9 +91,28 @@ public void init(FilterConfig filterConfig) {
}
+ /**
+ * @deprecated This class is not registered as a filter any more, and nothing in the library calls this method.
+ * It runs Jersey and ignores the chain, because Jersey writes the response: a filter placed after it could
+ * only run once the resource had already executed. Register filters on the servlet context instead, through
+ * {@link com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler#onStartup}, and
+ * they will run before Jersey.
+ */
+ @Deprecated
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
+ handle(servletRequest, servletResponse);
+ }
+
+ /**
+ * Runs the request through Jersey and waits for the response to be written. {@link JerseyLambdaContainerHandler}
+ * calls this from the servlet at the end of the filter chain, so the resource is only reached once every filter
+ * registered with the servlet context has passed the request on.
+ * @param servletRequest The incoming servlet request
+ * @param servletResponse The response Jersey writes to
+ */
+ void handle(ServletRequest servletRequest, ServletResponse servletResponse) {
Timer.start("JERSEY_FILTER_DOFILTER");
// we use a latch to make the processing inside Jersey synchronous
CountDownLatch jerseyLatch = new CountDownLatch(1);
@@ -106,7 +130,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
throw new InternalServerErrorException(e);
}
Timer.stop("JERSEY_FILTER_DOFILTER");
- filterChain.doFilter(servletRequest, servletResponse);
}
@Override
diff --git a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java
index 43bd7eac1..d894ff9a3 100644
--- a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java
+++ b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java
@@ -30,15 +30,16 @@
import org.glassfish.jersey.process.internal.RequestScoped;
import org.glassfish.jersey.server.ResourceConfig;
-import jakarta.servlet.DispatcherType;
-import jakarta.servlet.FilterRegistration;
import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletRegistration;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.Application;
-import java.util.EnumSet;
import java.util.concurrent.CountDownLatch;
@@ -72,7 +73,10 @@ public class JerseyLambdaContainerHandler extends Aws
// Variables - Private
//-------------------------------------------------------------
+ private static final String JERSEY_SERVLET_NAME = "JerseyServlet";
+
private JerseyHandlerFilter jerseyFilter;
+ private JerseyServlet jerseyServlet;
private boolean initialized;
//-------------------------------------------------------------
@@ -174,6 +178,7 @@ protected void configure() {
}
this.jerseyFilter = new JerseyHandlerFilter(jaxRsApplication);
+ this.jerseyServlet = new JerseyServlet(jerseyFilter);
Timer.stop("JERSEY_CONTAINER_CONSTRUCTOR");
}
@@ -194,7 +199,7 @@ protected void handleRequest(HttpServletRequest httpServletRequest, AwsHttpServl
((AwsHttpServletRequest)httpServletRequest).setServletContext(getServletContext());
}
- doFilter(httpServletRequest, httpServletResponse, null);
+ doFilter(httpServletRequest, httpServletResponse, jerseyServlet);
Timer.stop("JERSEY_HANDLE_REQUEST");
}
@@ -207,12 +212,13 @@ protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request
public void initialize() {
Timer.start("JERSEY_COLD_START_INIT");
- // manually add the filter to the chain. This should the last one and match all uris
- FilterRegistration.Dynamic jerseyFilterReg = getServletContext().addFilter("JerseyFilter", jerseyFilter);
- jerseyFilterReg.addMappingForUrlPatterns(
- EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.INCLUDE, DispatcherType.FORWARD),
- true, "/*"
- );
+ // Jersey runs as the servlet at the end of the filter chain, the way DispatcherServlet does for Spring. Every
+ // filter registered with the servlet context, before or after this method, runs first and can stop the
+ // request before it reaches a resource.
+ ServletRegistration.Dynamic jerseyServletReg = getServletContext().addServlet(JERSEY_SERVLET_NAME, jerseyServlet);
+ jerseyServletReg.addMapping("/*");
+ // the filter chain reads the servlet name from its config, so the servlet has to be initialized up front
+ jerseyServlet.init(((AwsServletRegistration) jerseyServletReg).getServletConfig());
Timer.stop("JERSEY_COLD_START_INIT");
initialized = true;
@@ -226,7 +232,53 @@ public InjectionManager getInjectionManager() {
return jerseyFilter.getApplicationHandler().getInjectionManager();
}
+ /**
+ * The servlet Jersey runs as, which is the terminal step of the filter chain for every request this handler
+ * routes. This returned null while Jersey ran as a filter and there was no servlet to hand back.
+ * @return The Jersey servlet
+ */
public Servlet getServlet() {
- return null;
+ return jerseyServlet;
+ }
+
+ //-------------------------------------------------------------
+ // Inner Class -
+ //-------------------------------------------------------------
+
+ /**
+ * The servlet at the end of the filter chain. It hands the request to Jersey, which writes the response.
+ */
+ private static final class JerseyServlet implements Servlet {
+ private final JerseyHandlerFilter jerseyFilter;
+ private ServletConfig config;
+
+ JerseyServlet(JerseyHandlerFilter jerseyFilter) {
+ this.jerseyFilter = jerseyFilter;
+ }
+
+ @Override
+ public void init(ServletConfig servletConfig) {
+ config = servletConfig;
+ }
+
+ @Override
+ public ServletConfig getServletConfig() {
+ return config;
+ }
+
+ @Override
+ public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
+ jerseyFilter.handle(servletRequest, servletResponse);
+ }
+
+ @Override
+ public String getServletInfo() {
+ return "";
+ }
+
+ @Override
+ public void destroy() {
+
+ }
}
}
diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyFilterChainTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyFilterChainTest.java
new file mode 100644
index 000000000..a8f0dbc23
--- /dev/null
+++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyFilterChainTest.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
+ * with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0/
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
+ * and limitations under the License.
+ */
+package com.amazonaws.serverless.proxy.jersey;
+
+import com.amazonaws.serverless.proxy.AwsHttpApiV2SecurityContextWriter;
+import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler;
+import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter;
+import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
+import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpApiV2HttpServletRequestReader;
+import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader;
+import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter;
+import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration;
+import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder;
+import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext;
+import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
+import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
+import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Servlet filters registered on a Jersey handler must run before the JAX-RS resource, as they would in front of a
+ * servlet in a servlet container. Filters added through onStartup after one of the default factory
+ * methods used to run only after Jersey had already executed the resource and written its response, so a filter that
+ * denied the request could not stop it.
+ *
+ * Requests go through proxyStream, the entry point a Lambda function's stream handler calls, which
+ * deserializes the event and hands it to proxy.
+ */
+public class JerseyFilterChainTest {
+ private static final String SECRET_BODY = "TOP_SECRET";
+ private static final String FORBIDDEN_BODY = "FORBIDDEN";
+ private static final String FILTER_ATTRIBUTE = "com.amazonaws.serverless.jersey.test.filterRan";
+ private static final String NO_ATTRIBUTE = "NO_ATTRIBUTE";
+ private static final String FILTER_HEADER = "X-Filter-Header";
+
+ private static final AtomicInteger RESOURCE_INVOCATIONS = new AtomicInteger(0);
+
+ private final MockLambdaContext lambdaContext = new MockLambdaContext();
+
+ public static Collection