Intercept This! Hands-On with HTTP Filters in Quarkus
Learn how to log, audit, and enhance your REST APIs using standard JAX-RS filters with Quarkus — no black magic required.
Let’s be honest: When it comes to enterprise Java, not everything needs to be reactive, bleeding-edge, or even flashy. Sometimes, you just need reliable, well-worn tools that do exactly what you want, predictably and efficiently. That’s what Quarkus gives you with its support for REST and standard JAX-RS filters.
In this tutorial, we’ll walk through building a Quarkus application that intercepts HTTP requests and responses both globally and conditionally using standard JAX-RS filters. You’ll learn how to implement logging, add response headers, and create a filter that only kicks in when you say so. All without leaving the warm embrace of Jakarta REST.
Whether you're a logging addict, a metrics hoarder, or just someone who loves seeing their own debug statements fly by in a terminal, this one’s for you.
What You’ll Build
We’re going to:
Log all incoming HTTP requests and outgoing responses.
Add custom headers to all responses.
Create an auditing filter that only runs on endpoints we mark with a special annotation.
Learn how Quarkus handles standard JAX-RS features like
@Provider
,@NameBinding
, and@Priority
.
Prerequisites
You’ll need:
JDK 11+ installed
Maven installed
Basic Java and REST knowledge
A terminal (or the courage to use one)
curl, Postman, or anything that hits HTTP endpoints
1. Create a New Quarkus Project with REST support
Let’s bootstrap our Quarkus project with REST features using the rest-jackson
extension:
mvn io.quarkus.platform:quarkus-maven-plugin:3.22.1:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=quarkus-interceptor-tutorial \
-Dextensions="rest-jackson"
cd quarkus-interceptor-tutorial
2. Global Request Filter: Log All the Things
Let’s create a filter that logs every incoming request. This is a global filter—applied to all endpoints.
File: src/main/java/org/acme/filters/GlobalLoggingRequestFilter.java
package org.acme.filters;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.io.IOException;
@Provider
public class GlobalLoggingRequestFilter implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(GlobalLoggingRequestFilter.class);
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
LOG.infof("[GLOBAL] Received request: %s %s",
requestContext.getMethod(),
requestContext.getUriInfo().getAbsolutePath());
requestContext.setProperty("request-start-time", System.nanoTime());
}
}
3. Global Response Filter: Stamp Every Response
Let’s add a custom header (X-Global-Trace-ID
) to every response and measure how long the request took.
File: src/main/java/org/acme/filters/GlobalHeaderResponseFilter.java
package org.acme.filters;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.util.UUID;
@Provider
public class GlobalHeaderResponseFilter implements ContainerResponseFilter {
private static final Logger LOG = Logger.getLogger(GlobalHeaderResponseFilter.class);
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
responseContext.getHeaders().add(
"X-Global-Trace-ID",
UUID.randomUUID().toString().substring(0, 8));
Object startTimeObj = requestContext.getProperty("request-start-time");
if (startTimeObj instanceof Long) {
long durationNanos = System.nanoTime() - (Long) startTimeObj;
LOG.infof("[GLOBAL] Request processed in %.2f ms", durationNanos / 1_000_000.0);
}
LOG.info("[GLOBAL] Response filter finished.");
}
}
4. Conditional Filters with Name Binding: Only When You Say So
Sometimes, you only want special handling for specific endpoints—like audit logging, authentication, or a “don’t embarrass me in production” sanity check.
That’s where Name Binding comes in.
4a. Define a Binding Annotation
File: src/main/java/org/acme/filters/Audited.java
package org.acme.filters;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.ws.rs.NameBinding;
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Audited {
}
4b. Implement the Conditional Filter
File: src/main/java/org/acme/AuditRequestFilter.java
package org.acme;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.io.IOException;
@Provider
@Audited
public class AuditRequestFilter implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(AuditRequestFilter.class);
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
LOG.infof("[AUDIT] Performing detailed audit check for: %s %s",
requestContext.getMethod(),
requestContext.getUriInfo().getPath());
}
}
Add
@Priority(...)
if you care about the order in which filters run. More on that later.
5. Mark the Endpoints
Let’s update GreetingResource
to mark some endpoints as @Audited
.
File: src/main/java/org/acme/GreetingResource.java
package org.acme;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.acme.filters.Audited;
import org.jboss.logging.Logger;
@Path("/hello")
public class GreetingResource {
private static final Logger LOG = Logger.getLogger(GreetingResource.class);
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
LOG.info("Executing standard hello() method.");
return "Hello from Quarkus";
}
@GET
@Path("/audited")
@Produces(MediaType.TEXT_PLAIN)
@Audited
public String helloAudited() {
LOG.info("Executing AUDITED helloAudited() method.");
return "Hello from the *audited* endpoint!";
}
}
6. Run and Observe the Magic
Start your app:
quarkus dev
In a second terminal:
echo "--- /hello (not audited) ---"
curl -v http://localhost:8080/hello
echo "\n\n--- /hello/audited ---"
curl -v http://localhost:8080/hello/audited
What You Should See
In the console:
/hello
: only global filters run./hello/audited
: global filters plus the audit filter.
In the curl
output:
Every response includes
X-Global-Trace-ID
.Audit filter doesn’t alter responses but logs its presence like a polite ninja.
7. Bonus Round: Fine-Tuning with Priorities
If you have multiple filters and care about their order, use @Priority
:
import jakarta.annotation.Priority;
@Provider
@Audited
@Priority(100) // Lower = runs earlier
public class AuditRequestFilter implements ContainerRequestFilter { ... }
Conclusion
You just built a solid HTTP interception setup in Quarkus using nothing but standard JAX-RS and Quarkus Rest. You learned how to:
Log and manipulate requests/responses globally.
Conditionally intercept requests with name binding.
Keep your filters modular, reusable, and testable.
This pattern works beautifully for logging, header injection, access control, request tracing, and any other cross-cutting concern in your app.
Next time you’re tempted to over-engineer an interceptor stack with libraries and spaghetti proxies remember: JAX-RS filters are simple, powerful, and already there.
Want to take this further?
Try:
Adding response filters for
@Audited
endpoints.Blocking requests based on user roles.
Emitting structured logs or metrics to Prometheus.
Welcome to the wonderful, filter-filled world of Quarkus.