REST framework offering the route model to define non blocking endpoints
—
Cross-cutting concern implementation using @RouteFilter annotation for handling authentication, logging, request modification, and other concerns that apply across multiple endpoints.
Defines filters that execute on every HTTP request with configurable priority ordering for comprehensive request/response processing.
/**
* Defines a filter that runs on every HTTP request
* Filters are executed in priority order (higher priority values run first)
* @param value Filter priority (default: DEFAULT_PRIORITY = 10)
*/
@RouteFilter(10) // or @RouteFilter(value = 10)
/**
* Filter methods must:
* - Take RoutingExchange as parameter
* - Return void or call exchange.context().next() to continue processing
* - Can modify request/response or terminate processing
*/
/** Default filter priority constant */
public static final int DEFAULT_PRIORITY = 10;Usage Examples:
import io.quarkus.vertx.web.RouteFilter;
import io.quarkus.vertx.web.RoutingExchange;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class FilterExamples {
// Basic request logging filter
@RouteFilter(10)
public void logRequests(RoutingExchange exchange) {
String method = exchange.request().method().name();
String path = exchange.request().path();
String userAgent = exchange.getHeader("User-Agent").orElse("Unknown");
System.out.printf("[%s] %s %s - %s%n",
Instant.now(), method, path, userAgent);
// Continue to next filter or route handler
exchange.context().next();
}
// CORS filter (high priority)
@RouteFilter(1)
public void corsFilter(RoutingExchange exchange) {
exchange.response()
.putHeader("Access-Control-Allow-Origin", "*")
.putHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
.putHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// Handle preflight OPTIONS requests
if ("OPTIONS".equals(exchange.request().method().name())) {
exchange.response().setStatusCode(200).end();
return; // Don't call next() - terminate here
}
exchange.context().next();
}
// Authentication filter (medium priority)
@RouteFilter(5)
public void authFilter(RoutingExchange exchange) {
String path = exchange.request().path();
// Skip authentication for public paths
if (path.startsWith("/public") || path.equals("/health")) {
exchange.context().next();
return;
}
String authHeader = exchange.getHeader("Authorization").orElse("");
if (!authHeader.startsWith("Bearer ")) {
exchange.response()
.setStatusCode(401)
.putHeader("WWW-Authenticate", "Bearer")
.end("Unauthorized");
return;
}
String token = authHeader.substring(7);
if (!isValidToken(token)) {
exchange.response().setStatusCode(403).end("Invalid token");
return;
}
// Add user info to context for downstream handlers
exchange.context().put("userId", extractUserIdFromToken(token));
exchange.context().next();
}
private boolean isValidToken(String token) {
// Token validation logic
return token.length() > 10 && !token.equals("invalid");
}
private String extractUserIdFromToken(String token) {
// Extract user ID from token
return "user-" + token.hashCode();
}
}Filters execute in priority order, with lower priority values executing first. Multiple filters with the same priority execute in undefined order.
@ApplicationScoped
public class OrderedFilters {
// First filter - security headers
@RouteFilter(1)
public void securityHeaders(RoutingExchange exchange) {
exchange.response()
.putHeader("X-Content-Type-Options", "nosniff")
.putHeader("X-Frame-Options", "DENY")
.putHeader("X-XSS-Protection", "1; mode=block");
exchange.context().next();
}
// Second filter - request timing
@RouteFilter(2)
public void startTiming(RoutingExchange exchange) {
exchange.context().put("startTime", System.currentTimeMillis());
exchange.context().next();
}
// Third filter - content negotiation
@RouteFilter(3)
public void contentNegotiation(RoutingExchange exchange) {
String accept = exchange.getHeader("Accept").orElse("*/*");
exchange.context().put("acceptHeader", accept);
exchange.context().next();
}
// Last filter - request completion timing
@RouteFilter(1000)
public void endTiming(RoutingExchange exchange) {
// This runs after route handler completion
Long startTime = exchange.context().get("startTime");
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
exchange.response().putHeader("X-Response-Time", duration + "ms");
}
exchange.context().next();
}
}Comprehensive authentication and authorization patterns using filters.
@ApplicationScoped
public class AuthFilters {
// API key authentication
@RouteFilter(5)
public void apiKeyAuth(RoutingExchange exchange) {
String path = exchange.request().path();
// Only apply to API paths
if (!path.startsWith("/api/")) {
exchange.context().next();
return;
}
String apiKey = exchange.getHeader("X-API-Key").orElse("");
if (apiKey.isEmpty()) {
exchange.response().setStatusCode(401)
.end("API key required");
return;
}
if (!isValidApiKey(apiKey)) {
exchange.response().setStatusCode(403)
.end("Invalid API key");
return;
}
exchange.context().put("apiKeyValid", true);
exchange.context().next();
}
// Role-based authorization
@RouteFilter(6)
public void roleBasedAuth(RoutingExchange exchange) {
String path = exchange.request().path();
String method = exchange.request().method().name();
// Admin-only paths
if (path.startsWith("/admin/")) {
String userRole = getUserRole(exchange);
if (!"admin".equals(userRole)) {
exchange.response().setStatusCode(403)
.end("Admin access required");
return;
}
}
// Write operations require elevated permissions
if (method.matches("POST|PUT|DELETE") && path.startsWith("/api/")) {
String userRole = getUserRole(exchange);
if ("readonly".equals(userRole)) {
exchange.response().setStatusCode(403)
.end("Write access denied");
return;
}
}
exchange.context().next();
}
// JWT token validation
@RouteFilter(4)
public void jwtValidation(RoutingExchange exchange) {
String authHeader = exchange.getHeader("Authorization").orElse("");
if (authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
JwtClaims claims = validateJwtToken(token);
exchange.context().put("jwtClaims", claims);
exchange.context().put("userId", claims.getSubject());
} catch (Exception e) {
exchange.response().setStatusCode(401)
.end("Invalid JWT token");
return;
}
}
exchange.context().next();
}
private boolean isValidApiKey(String apiKey) {
// API key validation logic
return apiKey.startsWith("ak_") && apiKey.length() == 32;
}
private String getUserRole(RoutingExchange exchange) {
// Extract user role from context or token
JwtClaims claims = exchange.context().get("jwtClaims");
return claims != null ? claims.getStringClaimValue("role") : "guest";
}
private JwtClaims validateJwtToken(String token) throws Exception {
// JWT validation logic (placeholder)
if (token.length() < 10) {
throw new Exception("Invalid token");
}
// Return mock claims
return new JwtClaims() {
public String getSubject() { return "user123"; }
public String getStringClaimValue(String claim) {
return "role".equals(claim) ? "user" : null;
}
};
}
// Mock JWT claims interface
private interface JwtClaims {
String getSubject();
String getStringClaimValue(String claim);
}
}Filters for modifying requests and responses, including content transformation and header manipulation.
@ApplicationScoped
public class ModificationFilters {
// Request body logging and modification
@RouteFilter(7)
public void requestModification(RoutingExchange exchange) {
String method = exchange.request().method().name();
// Only process request bodies for write operations
if (!method.matches("POST|PUT|PATCH")) {
exchange.context().next();
return;
}
// Log request body (in real implementation, be careful with sensitive data)
exchange.request().body().onSuccess(buffer -> {
if (buffer != null && buffer.length() > 0) {
System.out.println("Request body size: " + buffer.length() + " bytes");
// Example: Add request ID to context
String requestId = "req-" + System.currentTimeMillis();
exchange.context().put("requestId", requestId);
exchange.response().putHeader("X-Request-ID", requestId);
}
exchange.context().next();
}).onFailure(throwable -> {
exchange.response().setStatusCode(400).end("Invalid request body");
});
}
// Response compression filter
@RouteFilter(999)
public void responseCompression(RoutingExchange exchange) {
String acceptEncoding = exchange.getHeader("Accept-Encoding").orElse("");
if (acceptEncoding.contains("gzip")) {
exchange.response().putHeader("Content-Encoding", "gzip");
// Note: Actual compression would be handled by Quarkus/Vert.x configuration
}
exchange.context().next();
}
// Content type enforcement
@RouteFilter(8)
public void contentTypeEnforcement(RoutingExchange exchange) {
String method = exchange.request().method().name();
String path = exchange.request().path();
// Enforce JSON content type for API endpoints
if (method.matches("POST|PUT|PATCH") && path.startsWith("/api/")) {
String contentType = exchange.getHeader("Content-Type").orElse("");
if (!contentType.contains("application/json")) {
exchange.response().setStatusCode(415)
.putHeader("Accept", "application/json")
.end("Content-Type must be application/json");
return;
}
}
exchange.context().next();
}
// Response headers standardization
@RouteFilter(900)
public void responseHeaders(RoutingExchange exchange) {
// Add standard response headers
exchange.response()
.putHeader("X-Powered-By", "Quarkus Reactive Routes")
.putHeader("X-Content-Type-Options", "nosniff");
// Add cache headers based on path
String path = exchange.request().path();
if (path.startsWith("/static/")) {
exchange.response().putHeader("Cache-Control", "public, max-age=86400");
} else if (path.startsWith("/api/")) {
exchange.response().putHeader("Cache-Control", "no-cache, no-store, must-revalidate");
}
exchange.context().next();
}
}Filters for comprehensive error handling, monitoring, and observability.
@ApplicationScoped
public class MonitoringFilters {
// Global error handling filter
@RouteFilter(value = 1001, type = Route.HandlerType.FAILURE)
public void globalErrorHandler(RoutingExchange exchange) {
Throwable failure = exchange.context().failure();
String requestId = exchange.context().get("requestId");
// Log error with context
System.err.printf("Error [%s]: %s - %s%n",
requestId, failure.getClass().getSimpleName(), failure.getMessage());
// Return appropriate error response
if (failure instanceof IllegalArgumentException) {
exchange.response().setStatusCode(400)
.end("Bad Request: " + failure.getMessage());
} else if (failure instanceof SecurityException) {
exchange.response().setStatusCode(403)
.end("Access Denied");
} else {
exchange.response().setStatusCode(500)
.end("Internal Server Error");
}
}
// Request metrics collection
@RouteFilter(2)
public void metricsCollection(RoutingExchange exchange) {
String method = exchange.request().method().name();
String path = exchange.request().path();
// Increment request counter (pseudo-code)
// MetricsRegistry.counter("http_requests_total", "method", method, "path", path).increment();
// Track concurrent requests
exchange.context().put("startTime", System.currentTimeMillis());
exchange.context().addBodyEndHandler(v -> {
Long startTime = exchange.context().get("startTime");
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
int statusCode = exchange.response().getStatusCode();
// Record response time histogram (pseudo-code)
// MetricsRegistry.timer("http_request_duration", "method", method, "status", String.valueOf(statusCode))
// .record(Duration.ofMillis(duration));
System.out.printf("Request completed: %s %s - %d (%dms)%n",
method, path, statusCode, duration);
}
});
exchange.context().next();
}
// Rate limiting filter
@RouteFilter(3)
public void rateLimiting(RoutingExchange exchange) {
String clientIp = getClientIp(exchange);
String path = exchange.request().path();
// Skip rate limiting for health checks
if (path.equals("/health") || path.equals("/ready")) {
exchange.context().next();
return;
}
// Check rate limit (pseudo-implementation)
if (isRateLimited(clientIp)) {
exchange.response()
.setStatusCode(429)
.putHeader("Retry-After", "60")
.end("Rate limit exceeded");
return;
}
exchange.context().next();
}
private String getClientIp(RoutingExchange exchange) {
// Check for forwarded IP headers
return exchange.getHeader("X-Forwarded-For")
.or(() -> exchange.getHeader("X-Real-IP"))
.orElse(exchange.request().remoteAddress().host());
}
private boolean isRateLimited(String clientIp) {
// Rate limiting logic (would use Redis, in-memory cache, etc.)
return false; // Placeholder
}
}Advanced patterns for filter configuration and sharing data between filters and route handlers.
@ApplicationScoped
public class AdvancedFilters {
// Context enrichment filter
@RouteFilter(4)
public void contextEnrichment(RoutingExchange exchange) {
String userAgent = exchange.getHeader("User-Agent").orElse("Unknown");
String acceptLanguage = exchange.getHeader("Accept-Language").orElse("en");
// Parse and add request context
RequestContext requestContext = new RequestContext(
userAgent,
parseLanguage(acceptLanguage),
isMobileClient(userAgent),
System.currentTimeMillis()
);
exchange.context().put("requestContext", requestContext);
exchange.context().next();
}
// Feature flag filter
@RouteFilter(6)
public void featureFlags(RoutingExchange exchange) {
String path = exchange.request().path();
RequestContext context = exchange.context().get("requestContext");
// Check feature flags based on request context
if (path.startsWith("/api/v2/") && !isFeatureEnabled("api_v2", context)) {
exchange.response().setStatusCode(404)
.end("API version not available");
return;
}
exchange.context().next();
}
private String parseLanguage(String acceptLanguage) {
return acceptLanguage.split(",")[0].trim();
}
private boolean isMobileClient(String userAgent) {
return userAgent.toLowerCase().contains("mobile");
}
private boolean isFeatureEnabled(String feature, RequestContext context) {
// Feature flag logic
return true; // Placeholder
}
// Request context data class
public static class RequestContext {
private final String userAgent;
private final String language;
private final boolean mobile;
private final long timestamp;
public RequestContext(String userAgent, String language, boolean mobile, long timestamp) {
this.userAgent = userAgent;
this.language = language;
this.mobile = mobile;
this.timestamp = timestamp;
}
// Getters
public String getUserAgent() { return userAgent; }
public String getLanguage() { return language; }
public boolean isMobile() { return mobile; }
public long getTimestamp() { return timestamp; }
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-quarkus--quarkus-reactive-routes