Skip to main content

Sling Servlet Filters

Introduction

Sling servlet filters sit between the Sling engine and the servlet (or script) that ultimately handles a request. They intercept every request that matches their configuration and can inspect or modify the request and response before and after the servlet executes. This is the same javax.servlet.Filter contract you know from the Servlet specification, but registered through OSGi annotations instead of web.xml.

Filters are ideal for cross-cutting concerns — logic that applies to many requests regardless of which servlet handles them. Typical examples include logging, security-header injection, authentication checks, and decoration-tag removal.

tip

Think of filters as middleware. They form a chain: each filter calls chain.doFilter() to pass control to the next filter (or to the servlet at the end of the chain). Skipping that call effectively blocks the request.

Filter Annotation

Register a filter with the @SlingServletFilter annotation from org.apache.sling.servlets.annotations. Pair it with the OSGi @Component annotation so the SCR runtime picks it up.

Available Attributes

AttributeTypeDescription
scopeSlingServletFilterScope[]When the filter fires (see Filter Scopes below).
patternStringRegex matched against the request path (e.g. /content/mysite/.*).
resourceTypesString[]Sling resource types the filter applies to.
extensionsString[]Request extensions (e.g. html, json).
methodsString[]HTTP methods (e.g. GET, POST).
selectorsString[]Sling selectors (e.g. infinity, model).

Full Annotated Example

AnnotatedFilterExample.java
import org.apache.sling.servlets.annotations.SlingServletFilter;
import org.apache.sling.servlets.annotations.SlingServletFilterScope;
import org.osgi.service.component.annotations.Component;

import javax.servlet.*;
import java.io.IOException;

@Component
@SlingServletFilter(
scope = {SlingServletFilterScope.REQUEST},
pattern = "/content/mysite/.*",
extensions = {"html", "json"},
methods = {"GET"},
selectors = {"model"}
)
public class AnnotatedFilterExample implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Called once when the filter is activated
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// Pre-processing — runs before the servlet
// ...

chain.doFilter(request, response);

// Post-processing — runs after the servlet
// ...
}

@Override
public void destroy() {
// Called once when the filter is deactivated
}
}

Filter Scopes

The scope attribute controls when in the Sling request lifecycle the filter is invoked.

ScopeFires duringTypical use case
REQUESTThe initial (main) HTTP requestLogging, auth checks, response headers
INCLUDE<sling:include> / data-sly-resource callsDecoration-tag removal, component-level manipulation
FORWARD<sling:forward> callsRedirect interception, request rewriting
ERRORError-handling dispatches (e.g. 404, 500 pages)Custom error-page logic, error logging
COMPONENTComponent rendering (both include and forward)Component-level decoration, profiling
warning

Choosing the wrong scope is a common source of bugs. A REQUEST-scoped filter will never fire for data-sly-resource includes — you need INCLUDE or COMPONENT for that.

You can specify multiple scopes on a single filter:

@SlingServletFilter(scope = {SlingServletFilterScope.REQUEST, SlingServletFilterScope.INCLUDE})

Request Timing / Logging Filter

A practical filter that measures and logs how long each request takes to process.

RequestTimingFilter.java
import org.apache.sling.servlets.annotations.SlingServletFilter;
import org.apache.sling.servlets.annotations.SlingServletFilterScope;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component(property = "service.ranking:Integer=1000")
@SlingServletFilter(
scope = {SlingServletFilterScope.REQUEST},
pattern = "/content/.*"
)
public class RequestTimingFilter implements Filter {

private static final Logger log = LoggerFactory.getLogger(RequestTimingFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// no-op
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

final long start = System.nanoTime();
final String uri = ((HttpServletRequest) request).getRequestURI();

try {
chain.doFilter(request, response);
} finally {
final long durationMs = (System.nanoTime() - start) / 1_000_000;
log.info("Request [{}] completed in {} ms", uri, durationMs);

if (durationMs > 3000) {
log.warn("Slow request detected: [{}] took {} ms", uri, durationMs);
}
}
}

@Override
public void destroy() {
// no-op
}
}
tip

Wrapping chain.doFilter() in a try / finally block guarantees that the timing is logged even when the downstream servlet throws an exception.

Response Header Filter (CORS / Security Headers)

Add security-related HTTP headers to every response without touching individual servlets or scripts.

SecurityHeaderFilter.java
import org.apache.sling.servlets.annotations.SlingServletFilter;
import org.apache.sling.servlets.annotations.SlingServletFilterScope;
import org.osgi.service.component.annotations.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component(property = "service.ranking:Integer=900")
@SlingServletFilter(
scope = {SlingServletFilterScope.REQUEST},
pattern = "/content/.*",
extensions = {"html"},
methods = {"GET"}
)
public class SecurityHeaderFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// no-op
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

final HttpServletResponse httpResponse = (HttpServletResponse) response;

// Security headers
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN");
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
httpResponse.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");

chain.doFilter(request, response);
}

@Override
public void destroy() {
// no-op
}
}
warning

Security headers set in a Sling filter may be overridden by the Dispatcher or a CDN. Coordinate with your infrastructure team and verify the final headers with browser dev-tools or curl -I.

Remove CQ Decoration Tags

AEM wraps every component include in a decoration <div> tag. This filter strips those tags on publish so experience-fragment markup stays clean.

CqDecorationFilter.java
/**
* Remove AEM component wrapper div on publish.
* https://github.com/Adobe-Consulting-Services/acs-aem-samples/blob/master/core/src/main/java/com/adobe/acs/samples/filters/impl/SampleSlingIncludeFilter.java
*/

@Component
@SlingServletFilter(
scope = {SlingServletFilterScope.INCLUDE},
pattern = "/content/experience-fragments/.*",
extensions = {"html"},
methods = {"GET"}
)
public class CqDecorationFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// do nothing
}

@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {

final WCMMode mode = WCMMode.fromRequest(request);
final IncludeOptions includeOptions = IncludeOptions.getOptions(request, true);

// Only execute in Publish mode
if (WCMMode.DISABLED.equals(mode)) {
log.debug("Removing component wrapping div.");
// Disable CQ Decoration on cq:includes or sling:includes, only in Publish mode
includeOptions.setDecorationTagName("");
}

chain.doFilter(request, response);
}

@Override
public void destroy() {
// do nothing
}
}
tip

This filter uses SlingServletFilterScope.INCLUDE because decoration tags are applied during component includes, not during the top-level request. A REQUEST-scoped filter would never see these includes.

Filter Ordering

When multiple filters match the same request, OSGi uses the service.ranking property to determine execution order. Higher ranking = executes first (closer to the client).

Set the ranking via the @Component annotation:

@Component(property = "service.ranking:Integer=100")

Example: Two Filters with Different Rankings

HighPriorityFilter.java
@Component(property = "service.ranking:Integer=1000")
@SlingServletFilter(scope = {SlingServletFilterScope.REQUEST}, pattern = "/content/.*")
public class HighPriorityFilter implements Filter {
// Executes FIRST — good for authentication checks
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// auth check here ...
chain.doFilter(req, res);
}
// init() and destroy() omitted for brevity
}
LowPriorityFilter.java
@Component(property = "service.ranking:Integer=100")
@SlingServletFilter(scope = {SlingServletFilterScope.REQUEST}, pattern = "/content/.*")
public class LowPriorityFilter implements Filter {
// Executes SECOND — good for response decoration
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(req, res);
// modify response here ...
}
// init() and destroy() omitted for brevity
}

The call order for a single request looks like:

Client → HighPriorityFilter (1000) → LowPriorityFilter (100) → Servlet → LowPriorityFilter → HighPriorityFilter → Client

Filter vs Servlet

Filters and servlets serve different purposes. Use this decision table to pick the right tool.

ConcernFilterServlet
Logging / metrics
Security headers
Authentication / authorization
Request/response transformation
Dedicated REST endpoint
Specific resource type handler
Custom JSON API
Data import / processing

Rule of thumb: if the logic applies to many requests regardless of content, use a filter. If the logic handles a specific endpoint or resource type, use a servlet.

Common Pitfalls

1. Forgetting chain.doFilter()

If you forget to call chain.doFilter(request, response), the request hangs — no servlet is ever invoked and the client receives no response (or a timeout).

// BAD — request will never reach the servlet
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
log.info("Processing request");
// Missing: chain.doFilter(req, res);
}

2. Overly Broad Pattern Matching

A pattern like "/.*" matches every request in the entire JCR. This causes a measurable performance hit because the filter code runs on author UI requests, DAM asset requests, and health-check calls — not just page content.

// BAD — matches everything
@SlingServletFilter(scope = {SlingServletFilterScope.REQUEST}, pattern = "/.*")

// GOOD — scoped to your site's content
@SlingServletFilter(scope = {SlingServletFilterScope.REQUEST}, pattern = "/content/mysite/.*")

3. Scope Confusion

  • Use REQUEST for the incoming HTTP request.
  • Use INCLUDE when you need to intercept data-sly-resource or <sling:include> calls.
  • Use COMPONENT if you want to catch both includes and forwards during rendering.

Mixing these up means your filter either never fires or fires far more often than expected.

4. Not Checking WCM Mode

Filters run on both author and publish unless you explicitly check. Accidentally stripping decoration tags on author breaks the editor UI.

final WCMMode mode = WCMMode.fromRequest(request);
if (WCMMode.DISABLED.equals(mode)) {
// publish-only logic
}

5. Thread Safety

Sling servlet filters are singletons — OSGi creates exactly one instance that handles all concurrent requests. Never store per-request state in instance fields.

// BAD — shared mutable state
private String currentUser;

// GOOD — use local variables inside doFilter()
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
String currentUser = ((HttpServletRequest) req).getRemoteUser();
// ...
}
warning

Violating thread safety in a filter can cause data leaks between users — one user may see another user's data.

See also