Skip to content

fix(jersey): run servlet filters before Jersey resources 1.x - #1725

Open
ckawl wants to merge 1 commit into
aws:1.xfrom
ckawl:fix/jersey-filter-chain-order-1.x
Open

ckawl wants to merge 1 commit into
aws:1.xfrom
ckawl:fix/jersey-filter-chain-order-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 archetype's Maven build uses 1.9.6-SNAPSHOT and its tests pass.
  • The archetype's build.gradle and the pet-store sample use the range [1.9,2.0.0-M1), which on my machine resolved a leftover 1.10.0-SNAPSHOT from the local Maven cache. So I also built copies pinned to 1.9.6-SNAPSHOT: the archetype project builds with Gradle and its tests pass, and the sample builds with Maven and Gradle. CI has no such leftover, so the range resolves this branch there.

Related

This is the 1.x backport. The production change is identical to the main PR apart from import lines. The test scaffolding differs in three ways, all forced by the older stack:

  • javax.servlet and javax.ws.rs imports instead of jakarta.
  • The test filters extend a small base class that implements init and destroy, because the Servlet 3.1 Filter interface has no default methods.
  • The response is parsed inside the existing try block, because Jackson 2's readValue throws a checked IOException.

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