Skip to content

fix(jersey): run servlet filters before Jersey resources 2.1.x - #1724

Open
ckawl wants to merge 2 commits into
aws:2.1.xfrom
ckawl:fix/jersey-filter-chain-order-2.1.x
Open

ckawl wants to merge 2 commits into
aws:2.1.xfrom
ckawl:fix/jersey-filter-chain-order-2.1.x

Conversation

@ckawl

@ckawl ckawl commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Issue #, if available:

Description of changes:

Servlet filters registered on a Jersey handler now run before the JAX-RS resource, the way they run in front of a servlet in a servlet container.

The problem

JerseyLambdaContainerHandler.initialize() registered Jersey as a filter mapped to /*, and JerseyHandlerFilter.doFilter() ran the resource before calling filterChain.doFilter(). The default factory methods (getAwsProxyHandler, getHttpApiV2ProxyHandler) call initialize() before returning, so any filter an application added afterwards, through onStartup() or getServletContext().addFilter(), was placed after Jersey. It ran only once the resource had executed and written its response. A filter that ended the request without calling chain.doFilter() could change the status code, but the resource had already run and its body was returned.

Building the handler with the constructor, registering filters, and then calling initialize() avoided this, but nothing documented that the order mattered.

The change

  • Jersey runs as a servlet (JerseyServlet, mapped to /*) passed to doFilter() as the terminal step of the chain. This is the same mechanism the Spring module uses for DispatcherServlet: FilterChainManager already appends the servlet after every matching filter, so filters run first and in registration order, however and whenever they were registered.
  • JerseyHandlerFilter.doFilter() no longer calls filterChain.doFilter() after Jersey has written the response. The Jersey call moved to a package-private handle() that the servlet uses.
  • The servlet is initialized in initialize() because the filter chain reads its name from the servlet config when it builds the chain. The base class's initialize() isn't reused for this because it throws a checked exception that Jersey's public initialize() doesn't declare.
  • Calling initialize() a second time used to throw a NullPointerException, because the duplicate filter registration returned null. It now works.

Reordering the calls inside JerseyHandlerFilter.doFilter() was not enough: when a filter ends the request, control just returns to the Jersey filter, which can't tell and runs the resource anyway.

Behavior changes for applications

  • A filter that never calls chain.doFilter() now stops the request before Jersey runs, which is standard servlet behavior. In applications whose filters forgot to continue the chain, those requests no longer reach Jersey.
  • Filters that set request attributes or wrap the request now affect what the resource sees, because they run first.
  • getServletContext().getFilterRegistration("JerseyFilter") returns null. Jersey is registered as the servlet JerseyServlet instead.

No changes outside the Jersey module.

Testing

  • New JerseyFilterChainTest, the same 96 cases as on main. Without the change, 48 fail and 3 error, the same as on main.
  • Jersey module: 198 tests, 11 skipped, coverage checks met, no new SpotBugs findings.
  • gha_build.sh jersey true true passes: the Jersey archetype project builds with Maven and Gradle against 2.1.7-SNAPSHOT and its tests pass, and the Jersey pet-store sample builds with Maven and Gradle.
  • The sample's pom.xml uses an uncapped range, [2.0-SNAPSHOT,), so its Maven build resolves the newest 3.x version rather than this branch (Gradle is capped below 3.0 and resolves 2.1.7-SNAPSHOT). A copy of the sample pinned to 2.1.7-SNAPSHOT also builds with Maven.

Related

This is the 2.x backport. The production change is identical to the main PR. The only scaffolding difference is in the new test: Jackson 2's readValue throws a checked IOException, so the response is parsed inside the existing try block.

Review suggestion: review the main PR for the logic, and this one for the scaffolding delta.

By submitting this pull request

  • I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • I confirm that I've made a best effort attempt to update all relevant documentation.

Jersey was registered as a filter mapped to /* when the handler was
initialized, and it ran the resource before passing the request on.
Filters that an application added afterwards, through onStartup() or
getServletContext().addFilter(), therefore ran only after the resource
had executed and written its response, so a filter that ended the
request without calling chain.doFilter() could not stop it.

Jersey now runs as the servlet at the end of the filter chain, the way
DispatcherServlet does in the Spring module. Every filter registered
with the servlet context runs first, in registration order, whether it
was added before or after initialize(). JerseyHandlerFilter no longer
continues the chain once Jersey has written the response.

Calling initialize() a second time no longer throws a
NullPointerException.
…ilter

Review feedback on aws#1723.

getServlet() returned null because Jersey ran as a filter and there was
no servlet to hand back. It now returns the servlet the handler routes
to, which is the one registered on the servlet context.

JerseyHandlerFilter is no longer registered as a filter, so its class
documentation no longer describes it as one, and doFilter() is
deprecated: it runs Jersey and ignores the chain, because Jersey writes
the response. The Filter interface stays implemented, since the class is
public.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant