CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-quarkus--quarkus-reactive-routes

REST framework offering the route model to define non blocking endpoints

Pending
Overview
Eval results
Files

request-filtering.mddocs/

Request Filtering

Cross-cutting concern implementation using @RouteFilter annotation for handling authentication, logging, request modification, and other concerns that apply across multiple endpoints.

Capabilities

@RouteFilter Annotation

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();
    }
}

Filter Execution Order

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();
    }
}

Authentication and Authorization Filters

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);
    }
}

Request/Response Modification Filters

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();
    }
}

Error Handling and Monitoring Filters

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

Filter Configuration and Context Sharing

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

docs

index.md

parameter-injection.md

reactive-streaming.md

request-context.md

request-filtering.md

route-declaration.md

tile.json