docs
reference
tessl install tessl/maven-io-quarkus--quarkus-resteasy-reactive@3.15.0A Jakarta REST implementation utilizing build time processing and Vert.x for high-performance REST endpoints with reactive capabilities in cloud-native environments.
Quarkus REST provides declarative request and response filters for cross-cutting concerns like authentication, authorization, logging, header manipulation, and request/response transformation.
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import org.jboss.resteasy.reactive.server.WithFormRead;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.core.*;Declarative request filter executed before the resource method. Can abort request processing by returning a Response.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServerRequestFilter {
int priority() default Priorities.USER; // Execution priority
boolean preMatching() default false; // Execute before resource matching
boolean nonBlocking() default false; // Force non-blocking execution
boolean readBody() default false; // Deprecated: Read body before filter
}Filter methods can inject the following parameters:
ContainerRequestContext - Request contextUriInfo - URI informationHttpHeaders - HTTP headersRequest - HTTP requestResourceInfo - Resource method info (full)SimpleResourceInfo - Resource method info (lightweight)void - Cannot abort requestResponse - Non-null aborts with responseRestResponse<T> - Non-null aborts with responseOptional<Response> - Present value aborts with responseOptional<RestResponse<T>> - Present value aborts with responseUni<Void> - Async, cannot abortUni<Response> - Async, non-null abortsUni<RestResponse<T>> - Async, non-null abortsDeclarative response filter executed after the resource method. Can modify the response.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServerResponseFilter {
int priority() default Priorities.USER; // Execution priority
}ContainerRequestContext - Request contextContainerResponseContext - Response context (required for modification)ResourceInfo - Resource method info (full)UriInfo - URI informationSimpleResourceInfo - Resource method info (lightweight)Throwable - Exception if thrown, null otherwisevoid - Standard synchronous filterUni<Void> - Async filterForces form body to be read before filter or endpoint execution.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WithFormRead {
}@ServerRequestFilter(priority = Priorities.AUTHENTICATION)
public Optional<RestResponse<String>> authenticate(HttpHeaders headers) {
String authHeader = headers.getHeaderString("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return Optional.of(RestResponse.status(401, "Missing or invalid token"));
}
String token = authHeader.substring(7);
if (!tokenService.isValid(token)) {
return Optional.of(RestResponse.status(401, "Invalid token"));
}
return Optional.empty(); // Continue request processing
}@ServerRequestFilter(priority = Priorities.AUTHORIZATION)
public Optional<Response> authorize(UriInfo uriInfo, SecurityContext securityContext) {
String path = uriInfo.getPath();
if (path.startsWith("/admin") && !securityContext.isUserInRole("ADMIN")) {
return Optional.of(Response.status(403).entity("Forbidden").build());
}
return Optional.empty();
}@ServerRequestFilter
public void logRequest(UriInfo uriInfo, HttpHeaders headers) {
String method = headers.getHeaderString("X-HTTP-Method-Override");
String path = uriInfo.getPath();
logger.info("Request: {} {}", method, path);
}Executes before resource method matching, useful for request transformation:
@ServerRequestFilter(preMatching = true)
public void preprocessRequest(ContainerRequestContext requestContext) {
// Modify URI before matching
String path = requestContext.getUriInfo().getPath();
if (path.startsWith("/v1/")) {
requestContext.setRequestUri(
requestContext.getUriInfo().getBaseUri(),
URI.create(path.replace("/v1/", "/api/"))
);
}
}@ServerRequestFilter
public Optional<Response> conditionalFilter(
UriInfo uriInfo,
SimpleResourceInfo resourceInfo
) {
// Only apply to specific resources
if (resourceInfo.getResourceClass().equals(SecureResource.class)) {
// Apply security check
if (!checkSecurity()) {
return Optional.of(Response.status(403).build());
}
}
return Optional.empty();
}@ServerRequestFilter
@WithFormRead
public Optional<Response> validateForm(ContainerRequestContext context) {
// Form body is read and available
MultivaluedMap<String, String> formParams =
context.getUriInfo().getQueryParameters();
if (!formParams.containsKey("required_field")) {
return Optional.of(Response.status(400)
.entity("Missing required field")
.build());
}
return Optional.empty();
}@ServerRequestFilter(priority = Priorities.AUTHENTICATION)
public Uni<Optional<RestResponse<String>>> authenticateAsync(HttpHeaders headers) {
String token = headers.getHeaderString("Authorization");
if (token == null) {
return Uni.createFrom().item(
Optional.of(RestResponse.status(401, "Missing token"))
);
}
return tokenService.validateAsync(token)
.map(valid -> valid
? Optional.empty()
: Optional.of(RestResponse.status(401, "Invalid token"))
);
}@ServerResponseFilter
public void addHeaders(ContainerResponseContext responseContext) {
responseContext.getHeaders().add("X-Custom-Header", "value");
responseContext.getHeaders().add("X-Response-Time", System.currentTimeMillis());
}@ServerResponseFilter
public void corsFilter(ContainerResponseContext response) {
response.getHeaders().add("Access-Control-Allow-Origin", "*");
response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.getHeaders().add("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.getHeaders().add("Access-Control-Max-Age", "3600");
}@ServerResponseFilter
public void logResponse(
ContainerRequestContext request,
ContainerResponseContext response,
Throwable thrown
) {
String path = request.getUriInfo().getPath();
int status = response.getStatus();
if (thrown != null) {
logger.error("Request failed: {} - {}", path, thrown.getMessage());
} else {
logger.info("Response: {} - {}", path, status);
}
}@ServerResponseFilter
public void wrapResponse(ContainerResponseContext response) {
if (response.hasEntity() && response.getStatus() == 200) {
Object entity = response.getEntity();
// Wrap entity in envelope
ResponseEnvelope envelope = new ResponseEnvelope();
envelope.setData(entity);
envelope.setTimestamp(Instant.now());
response.setEntity(envelope);
}
}@ServerResponseFilter
public void handleErrors(
ContainerResponseContext response,
Throwable thrown
) {
if (thrown != null) {
ErrorResponse error = new ErrorResponse();
error.setMessage(thrown.getMessage());
error.setTimestamp(Instant.now());
response.setStatus(500);
response.setEntity(error);
}
}@ServerResponseFilter
public void conditionalModification(
ContainerResponseContext response,
ResourceInfo resourceInfo
) {
// Only modify responses from specific resources
if (resourceInfo.getResourceClass().equals(ApiResource.class)) {
response.getHeaders().add("X-API-Version", "1.0");
}
}@ServerResponseFilter
public Uni<Void> asyncFilter(ContainerResponseContext response) {
return metricsService.recordResponseAsync(response.getStatus())
.replaceWithVoid();
}Filters execute in priority order. Use jakarta.ws.rs.Priorities constants:
public class Priorities {
public static final int AUTHENTICATION = 1000;
public static final int AUTHORIZATION = 2000;
public static final int HEADER_DECORATOR = 3000;
public static final int ENTITY_CODER = 4000;
public static final int USER = 5000;
}Request filters: Lower priority executes first (1000 before 5000) Response filters: Higher priority executes first (5000 before 1000)
@ServerRequestFilter(priority = Priorities.AUTHENTICATION) // Executes first
public Optional<Response> authenticate() { ... }
@ServerRequestFilter(priority = Priorities.AUTHORIZATION) // Executes second
public Optional<Response> authorize() { ... }
@ServerRequestFilter(priority = Priorities.USER) // Executes last
public void customFilter() { ... }Defined at the class level (outside resource classes) and apply to all requests:
@ApplicationScoped
public class GlobalFilters {
@ServerRequestFilter
public Optional<Response> globalAuth(HttpHeaders headers) {
// Applies to all requests
}
}Defined within resource classes and only apply to that resource:
@Path("/items")
public class ItemResource {
@ServerRequestFilter
public Optional<Response> localAuth(HttpHeaders headers) {
// Only applies to /items/* endpoints
}
@GET
public List<Item> list() { ... }
}Request filters can abort processing by returning a non-empty response:
// Abort with Response
@ServerRequestFilter
public Optional<Response> filter() {
if (shouldAbort()) {
return Optional.of(Response.status(403).build());
}
return Optional.empty();
}
// Abort with RestResponse
@ServerRequestFilter
public Optional<RestResponse<String>> filter() {
if (shouldAbort()) {
return Optional.of(RestResponse.status(403, "Forbidden"));
}
return Optional.empty();
}
// Cannot abort (void return)
@ServerRequestFilter
public void filter() {
// Can only modify request, cannot abort
}By default, filters execute on the I/O thread. Use nonBlocking = false to force worker thread execution:
@ServerRequestFilter(nonBlocking = false)
public Optional<Response> blockingFilter() {
// Executes on worker thread
// Safe for blocking operations
}For reactive filters, use Uni return type:
@ServerRequestFilter
public Uni<Optional<Response>> reactiveFilter() {
return someAsyncOperation()
.map(result -> result.isValid()
? Optional.empty()
: Optional.of(Response.status(400).build())
);
}