0
# Request Filtering
1
2
Cross-cutting concern implementation using `@RouteFilter` annotation for handling authentication, logging, request modification, and other concerns that apply across multiple endpoints.
3
4
## Capabilities
5
6
### @RouteFilter Annotation
7
8
Defines filters that execute on every HTTP request with configurable priority ordering for comprehensive request/response processing.
9
10
```java { .api }
11
/**
12
* Defines a filter that runs on every HTTP request
13
* Filters are executed in priority order (higher priority values run first)
14
* @param value Filter priority (default: DEFAULT_PRIORITY = 10)
15
*/
16
@RouteFilter(10) // or @RouteFilter(value = 10)
17
/**
18
* Filter methods must:
19
* - Take RoutingExchange as parameter
20
* - Return void or call exchange.context().next() to continue processing
21
* - Can modify request/response or terminate processing
22
*/
23
24
/** Default filter priority constant */
25
public static final int DEFAULT_PRIORITY = 10;
26
```
27
28
**Usage Examples:**
29
30
```java
31
import io.quarkus.vertx.web.RouteFilter;
32
import io.quarkus.vertx.web.RoutingExchange;
33
import jakarta.enterprise.context.ApplicationScoped;
34
35
@ApplicationScoped
36
public class FilterExamples {
37
38
// Basic request logging filter
39
@RouteFilter(10)
40
public void logRequests(RoutingExchange exchange) {
41
String method = exchange.request().method().name();
42
String path = exchange.request().path();
43
String userAgent = exchange.getHeader("User-Agent").orElse("Unknown");
44
45
System.out.printf("[%s] %s %s - %s%n",
46
Instant.now(), method, path, userAgent);
47
48
// Continue to next filter or route handler
49
exchange.context().next();
50
}
51
52
// CORS filter (high priority)
53
@RouteFilter(1)
54
public void corsFilter(RoutingExchange exchange) {
55
exchange.response()
56
.putHeader("Access-Control-Allow-Origin", "*")
57
.putHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
58
.putHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
59
60
// Handle preflight OPTIONS requests
61
if ("OPTIONS".equals(exchange.request().method().name())) {
62
exchange.response().setStatusCode(200).end();
63
return; // Don't call next() - terminate here
64
}
65
66
exchange.context().next();
67
}
68
69
// Authentication filter (medium priority)
70
@RouteFilter(5)
71
public void authFilter(RoutingExchange exchange) {
72
String path = exchange.request().path();
73
74
// Skip authentication for public paths
75
if (path.startsWith("/public") || path.equals("/health")) {
76
exchange.context().next();
77
return;
78
}
79
80
String authHeader = exchange.getHeader("Authorization").orElse("");
81
if (!authHeader.startsWith("Bearer ")) {
82
exchange.response()
83
.setStatusCode(401)
84
.putHeader("WWW-Authenticate", "Bearer")
85
.end("Unauthorized");
86
return;
87
}
88
89
String token = authHeader.substring(7);
90
if (!isValidToken(token)) {
91
exchange.response().setStatusCode(403).end("Invalid token");
92
return;
93
}
94
95
// Add user info to context for downstream handlers
96
exchange.context().put("userId", extractUserIdFromToken(token));
97
exchange.context().next();
98
}
99
100
private boolean isValidToken(String token) {
101
// Token validation logic
102
return token.length() > 10 && !token.equals("invalid");
103
}
104
105
private String extractUserIdFromToken(String token) {
106
// Extract user ID from token
107
return "user-" + token.hashCode();
108
}
109
}
110
```
111
112
### Filter Execution Order
113
114
Filters execute in priority order, with lower priority values executing first. Multiple filters with the same priority execute in undefined order.
115
116
```java
117
@ApplicationScoped
118
public class OrderedFilters {
119
120
// First filter - security headers
121
@RouteFilter(1)
122
public void securityHeaders(RoutingExchange exchange) {
123
exchange.response()
124
.putHeader("X-Content-Type-Options", "nosniff")
125
.putHeader("X-Frame-Options", "DENY")
126
.putHeader("X-XSS-Protection", "1; mode=block");
127
exchange.context().next();
128
}
129
130
// Second filter - request timing
131
@RouteFilter(2)
132
public void startTiming(RoutingExchange exchange) {
133
exchange.context().put("startTime", System.currentTimeMillis());
134
exchange.context().next();
135
}
136
137
// Third filter - content negotiation
138
@RouteFilter(3)
139
public void contentNegotiation(RoutingExchange exchange) {
140
String accept = exchange.getHeader("Accept").orElse("*/*");
141
exchange.context().put("acceptHeader", accept);
142
exchange.context().next();
143
}
144
145
// Last filter - request completion timing
146
@RouteFilter(1000)
147
public void endTiming(RoutingExchange exchange) {
148
// This runs after route handler completion
149
Long startTime = exchange.context().get("startTime");
150
if (startTime != null) {
151
long duration = System.currentTimeMillis() - startTime;
152
exchange.response().putHeader("X-Response-Time", duration + "ms");
153
}
154
exchange.context().next();
155
}
156
}
157
```
158
159
### Authentication and Authorization Filters
160
161
Comprehensive authentication and authorization patterns using filters.
162
163
```java
164
@ApplicationScoped
165
public class AuthFilters {
166
167
// API key authentication
168
@RouteFilter(5)
169
public void apiKeyAuth(RoutingExchange exchange) {
170
String path = exchange.request().path();
171
172
// Only apply to API paths
173
if (!path.startsWith("/api/")) {
174
exchange.context().next();
175
return;
176
}
177
178
String apiKey = exchange.getHeader("X-API-Key").orElse("");
179
if (apiKey.isEmpty()) {
180
exchange.response().setStatusCode(401)
181
.end("API key required");
182
return;
183
}
184
185
if (!isValidApiKey(apiKey)) {
186
exchange.response().setStatusCode(403)
187
.end("Invalid API key");
188
return;
189
}
190
191
exchange.context().put("apiKeyValid", true);
192
exchange.context().next();
193
}
194
195
// Role-based authorization
196
@RouteFilter(6)
197
public void roleBasedAuth(RoutingExchange exchange) {
198
String path = exchange.request().path();
199
String method = exchange.request().method().name();
200
201
// Admin-only paths
202
if (path.startsWith("/admin/")) {
203
String userRole = getUserRole(exchange);
204
if (!"admin".equals(userRole)) {
205
exchange.response().setStatusCode(403)
206
.end("Admin access required");
207
return;
208
}
209
}
210
211
// Write operations require elevated permissions
212
if (method.matches("POST|PUT|DELETE") && path.startsWith("/api/")) {
213
String userRole = getUserRole(exchange);
214
if ("readonly".equals(userRole)) {
215
exchange.response().setStatusCode(403)
216
.end("Write access denied");
217
return;
218
}
219
}
220
221
exchange.context().next();
222
}
223
224
// JWT token validation
225
@RouteFilter(4)
226
public void jwtValidation(RoutingExchange exchange) {
227
String authHeader = exchange.getHeader("Authorization").orElse("");
228
229
if (authHeader.startsWith("Bearer ")) {
230
String token = authHeader.substring(7);
231
try {
232
JwtClaims claims = validateJwtToken(token);
233
exchange.context().put("jwtClaims", claims);
234
exchange.context().put("userId", claims.getSubject());
235
} catch (Exception e) {
236
exchange.response().setStatusCode(401)
237
.end("Invalid JWT token");
238
return;
239
}
240
}
241
242
exchange.context().next();
243
}
244
245
private boolean isValidApiKey(String apiKey) {
246
// API key validation logic
247
return apiKey.startsWith("ak_") && apiKey.length() == 32;
248
}
249
250
private String getUserRole(RoutingExchange exchange) {
251
// Extract user role from context or token
252
JwtClaims claims = exchange.context().get("jwtClaims");
253
return claims != null ? claims.getStringClaimValue("role") : "guest";
254
}
255
256
private JwtClaims validateJwtToken(String token) throws Exception {
257
// JWT validation logic (placeholder)
258
if (token.length() < 10) {
259
throw new Exception("Invalid token");
260
}
261
// Return mock claims
262
return new JwtClaims() {
263
public String getSubject() { return "user123"; }
264
public String getStringClaimValue(String claim) {
265
return "role".equals(claim) ? "user" : null;
266
}
267
};
268
}
269
270
// Mock JWT claims interface
271
private interface JwtClaims {
272
String getSubject();
273
String getStringClaimValue(String claim);
274
}
275
}
276
```
277
278
### Request/Response Modification Filters
279
280
Filters for modifying requests and responses, including content transformation and header manipulation.
281
282
```java
283
@ApplicationScoped
284
public class ModificationFilters {
285
286
// Request body logging and modification
287
@RouteFilter(7)
288
public void requestModification(RoutingExchange exchange) {
289
String method = exchange.request().method().name();
290
291
// Only process request bodies for write operations
292
if (!method.matches("POST|PUT|PATCH")) {
293
exchange.context().next();
294
return;
295
}
296
297
// Log request body (in real implementation, be careful with sensitive data)
298
exchange.request().body().onSuccess(buffer -> {
299
if (buffer != null && buffer.length() > 0) {
300
System.out.println("Request body size: " + buffer.length() + " bytes");
301
302
// Example: Add request ID to context
303
String requestId = "req-" + System.currentTimeMillis();
304
exchange.context().put("requestId", requestId);
305
exchange.response().putHeader("X-Request-ID", requestId);
306
}
307
exchange.context().next();
308
}).onFailure(throwable -> {
309
exchange.response().setStatusCode(400).end("Invalid request body");
310
});
311
}
312
313
// Response compression filter
314
@RouteFilter(999)
315
public void responseCompression(RoutingExchange exchange) {
316
String acceptEncoding = exchange.getHeader("Accept-Encoding").orElse("");
317
318
if (acceptEncoding.contains("gzip")) {
319
exchange.response().putHeader("Content-Encoding", "gzip");
320
// Note: Actual compression would be handled by Quarkus/Vert.x configuration
321
}
322
323
exchange.context().next();
324
}
325
326
// Content type enforcement
327
@RouteFilter(8)
328
public void contentTypeEnforcement(RoutingExchange exchange) {
329
String method = exchange.request().method().name();
330
String path = exchange.request().path();
331
332
// Enforce JSON content type for API endpoints
333
if (method.matches("POST|PUT|PATCH") && path.startsWith("/api/")) {
334
String contentType = exchange.getHeader("Content-Type").orElse("");
335
336
if (!contentType.contains("application/json")) {
337
exchange.response().setStatusCode(415)
338
.putHeader("Accept", "application/json")
339
.end("Content-Type must be application/json");
340
return;
341
}
342
}
343
344
exchange.context().next();
345
}
346
347
// Response headers standardization
348
@RouteFilter(900)
349
public void responseHeaders(RoutingExchange exchange) {
350
// Add standard response headers
351
exchange.response()
352
.putHeader("X-Powered-By", "Quarkus Reactive Routes")
353
.putHeader("X-Content-Type-Options", "nosniff");
354
355
// Add cache headers based on path
356
String path = exchange.request().path();
357
if (path.startsWith("/static/")) {
358
exchange.response().putHeader("Cache-Control", "public, max-age=86400");
359
} else if (path.startsWith("/api/")) {
360
exchange.response().putHeader("Cache-Control", "no-cache, no-store, must-revalidate");
361
}
362
363
exchange.context().next();
364
}
365
}
366
```
367
368
### Error Handling and Monitoring Filters
369
370
Filters for comprehensive error handling, monitoring, and observability.
371
372
```java
373
@ApplicationScoped
374
public class MonitoringFilters {
375
376
// Global error handling filter
377
@RouteFilter(value = 1001, type = Route.HandlerType.FAILURE)
378
public void globalErrorHandler(RoutingExchange exchange) {
379
Throwable failure = exchange.context().failure();
380
String requestId = exchange.context().get("requestId");
381
382
// Log error with context
383
System.err.printf("Error [%s]: %s - %s%n",
384
requestId, failure.getClass().getSimpleName(), failure.getMessage());
385
386
// Return appropriate error response
387
if (failure instanceof IllegalArgumentException) {
388
exchange.response().setStatusCode(400)
389
.end("Bad Request: " + failure.getMessage());
390
} else if (failure instanceof SecurityException) {
391
exchange.response().setStatusCode(403)
392
.end("Access Denied");
393
} else {
394
exchange.response().setStatusCode(500)
395
.end("Internal Server Error");
396
}
397
}
398
399
// Request metrics collection
400
@RouteFilter(2)
401
public void metricsCollection(RoutingExchange exchange) {
402
String method = exchange.request().method().name();
403
String path = exchange.request().path();
404
405
// Increment request counter (pseudo-code)
406
// MetricsRegistry.counter("http_requests_total", "method", method, "path", path).increment();
407
408
// Track concurrent requests
409
exchange.context().put("startTime", System.currentTimeMillis());
410
exchange.context().addBodyEndHandler(v -> {
411
Long startTime = exchange.context().get("startTime");
412
if (startTime != null) {
413
long duration = System.currentTimeMillis() - startTime;
414
int statusCode = exchange.response().getStatusCode();
415
416
// Record response time histogram (pseudo-code)
417
// MetricsRegistry.timer("http_request_duration", "method", method, "status", String.valueOf(statusCode))
418
// .record(Duration.ofMillis(duration));
419
420
System.out.printf("Request completed: %s %s - %d (%dms)%n",
421
method, path, statusCode, duration);
422
}
423
});
424
425
exchange.context().next();
426
}
427
428
// Rate limiting filter
429
@RouteFilter(3)
430
public void rateLimiting(RoutingExchange exchange) {
431
String clientIp = getClientIp(exchange);
432
String path = exchange.request().path();
433
434
// Skip rate limiting for health checks
435
if (path.equals("/health") || path.equals("/ready")) {
436
exchange.context().next();
437
return;
438
}
439
440
// Check rate limit (pseudo-implementation)
441
if (isRateLimited(clientIp)) {
442
exchange.response()
443
.setStatusCode(429)
444
.putHeader("Retry-After", "60")
445
.end("Rate limit exceeded");
446
return;
447
}
448
449
exchange.context().next();
450
}
451
452
private String getClientIp(RoutingExchange exchange) {
453
// Check for forwarded IP headers
454
return exchange.getHeader("X-Forwarded-For")
455
.or(() -> exchange.getHeader("X-Real-IP"))
456
.orElse(exchange.request().remoteAddress().host());
457
}
458
459
private boolean isRateLimited(String clientIp) {
460
// Rate limiting logic (would use Redis, in-memory cache, etc.)
461
return false; // Placeholder
462
}
463
}
464
```
465
466
### Filter Configuration and Context Sharing
467
468
Advanced patterns for filter configuration and sharing data between filters and route handlers.
469
470
```java
471
@ApplicationScoped
472
public class AdvancedFilters {
473
474
// Context enrichment filter
475
@RouteFilter(4)
476
public void contextEnrichment(RoutingExchange exchange) {
477
String userAgent = exchange.getHeader("User-Agent").orElse("Unknown");
478
String acceptLanguage = exchange.getHeader("Accept-Language").orElse("en");
479
480
// Parse and add request context
481
RequestContext requestContext = new RequestContext(
482
userAgent,
483
parseLanguage(acceptLanguage),
484
isMobileClient(userAgent),
485
System.currentTimeMillis()
486
);
487
488
exchange.context().put("requestContext", requestContext);
489
exchange.context().next();
490
}
491
492
// Feature flag filter
493
@RouteFilter(6)
494
public void featureFlags(RoutingExchange exchange) {
495
String path = exchange.request().path();
496
RequestContext context = exchange.context().get("requestContext");
497
498
// Check feature flags based on request context
499
if (path.startsWith("/api/v2/") && !isFeatureEnabled("api_v2", context)) {
500
exchange.response().setStatusCode(404)
501
.end("API version not available");
502
return;
503
}
504
505
exchange.context().next();
506
}
507
508
private String parseLanguage(String acceptLanguage) {
509
return acceptLanguage.split(",")[0].trim();
510
}
511
512
private boolean isMobileClient(String userAgent) {
513
return userAgent.toLowerCase().contains("mobile");
514
}
515
516
private boolean isFeatureEnabled(String feature, RequestContext context) {
517
// Feature flag logic
518
return true; // Placeholder
519
}
520
521
// Request context data class
522
public static class RequestContext {
523
private final String userAgent;
524
private final String language;
525
private final boolean mobile;
526
private final long timestamp;
527
528
public RequestContext(String userAgent, String language, boolean mobile, long timestamp) {
529
this.userAgent = userAgent;
530
this.language = language;
531
this.mobile = mobile;
532
this.timestamp = timestamp;
533
}
534
535
// Getters
536
public String getUserAgent() { return userAgent; }
537
public String getLanguage() { return language; }
538
public boolean isMobile() { return mobile; }
539
public long getTimestamp() { return timestamp; }
540
}
541
}
542
```