Skip to main content

Filters

Filters are the key to implementing cross-cutting concerns in Sproogy. They provide a powerful way to intercept requests and responses before they reach controllers.

What Are Filters?

Filters implement the Chain of Responsibility pattern. Each filter can:

  • Pre-process requests (before the controller)
  • Post-process responses (after the controller)
  • Short-circuit the chain (block requests)
  • Modify request/response data

Think of filters as middleware in Express.js or interceptors in Angular.

Filter Interface

public interface Filter {
void doFilter(Request request, Response response, FilterChain chain) throws Exception;
}

Parameters:

  • request: Incoming request (read/write)
  • response: Outgoing response (write only before chain, read/write after chain)
  • chain: Call chain.doFilter() to continue to next filter

Filter Execution Order

Filters execute in a stack-based order:

Execution:

  1. Filter 1 pre-processing
  2. Filter 2 pre-processing
  3. Filter 3 pre-processing
  4. Controller executes
  5. Filter 3 post-processing
  6. Filter 2 post-processing
  7. Filter 1 post-processing

Creating a Filter

Step 1: Implement the Interface

@Component
public class LoggingFilter implements Filter {

@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
// PRE-PROCESSING
long startTime = System.currentTimeMillis();
System.out.println("[REQUEST] " + request.getEndpoint());

// CONTINUE CHAIN
chain.doFilter(request, response);

// POST-PROCESSING
long duration = System.currentTimeMillis() - startTime;
System.out.println("[RESPONSE] " + response.getStatus() + " (" + duration + "ms)");
}
}

Step 2: Register as Component

Use @Component to let Sproogy discover and register the filter automatically.

Common Filter Patterns

1. Authentication Filter

Check credentials before allowing access:

@Component
public class AuthenticationFilter implements Filter {

@Autowired
private TokenService tokenService;

@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
String authHeader = request.getHeaders().get("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(401);
response.setData("Missing or invalid Authorization header");
return; // ❌ Don't call chain.doFilter() = request blocked
}

String token = authHeader.substring(7); // Remove "Bearer "

if (!tokenService.validateToken(token)) {
response.setStatus(401);
response.setData("Invalid token");
return; // ❌ Blocked
}

// Extract user info and add to request headers (for controllers to use)
String userId = tokenService.getUserId(token);
request.getHeaders().put("X-User-Id", userId);

// ✅ Token valid, continue
chain.doFilter(request, response);
}
}

Usage in controller:

@AppMapping("/profile")
public ResponseEntity<Profile> getProfile(Request request) {
String userId = request.getHeaders().get("X-User-Id"); // Injected by filter
return ResponseEntity.ok(profileService.getProfile(userId));
}

2. Rate Limiting Filter

Prevent abuse with rate limiting:

@Component
public class RateLimitFilter implements Filter {

private final Map<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();

@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
String clientId = extractClientId(request);

RateLimiter limiter = rateLimiters.computeIfAbsent(clientId, k ->
RateLimiter.create(10.0) // 10 requests per second
);

if (!limiter.tryAcquire()) {
response.setStatus(429); // Too Many Requests
response.setData("Rate limit exceeded. Try again later.");
response.getHeaders().put("Retry-After", "1");
return; // Blocked
}

chain.doFilter(request, response);
}

private String extractClientId(Request request) {
String auth = request.getHeaders().get("Authorization");
return auth != null ? auth : "anonymous";
}
}

3. Encryption Filter

Decrypt requests, encrypt responses:

@Component
public class EncryptionFilter implements Filter {

@Autowired
private EncryptionService encryptionService;

@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
// PRE: Decrypt request data if encrypted
String encryptedFlag = request.getHeaders().get("X-Encrypted");
if ("true".equals(encryptedFlag)) {
Object decryptedData = encryptionService.decrypt(request.getData());
request.setData(decryptedData);
}

// Continue chain
chain.doFilter(request, response);

// POST: Encrypt response if client expects it
if ("true".equals(encryptedFlag)) {
Object encryptedData = encryptionService.encrypt(response.getData());
response.setData(encryptedData);
response.getHeaders().put("X-Encrypted", "true");
}
}
}

Best Practices

Keep filters focused: One concern per filter

Always call chain.doFilter(): Unless you intentionally block the request

Handle exceptions: Prevent one filter from breaking the entire chain

Use meaningful status codes: 400, 401, 403, 429, 500

Log important events: Auth failures, rate limit violations

Next Steps

👉 Routing

👉 Custom Transformers