How to Block AI Bots on Quarkus (Java): Complete 2026 Guide
Quarkus is the cloud-native Java framework — supersonic, subatomic, built for Kubernetes. It has two filter APIs: the modern @ServerRequestFilter for RESTEasy Reactive (quarkus-resteasy-reactive) and the classic ContainerRequestFilter JAX-RS interface for quarkus-resteasy. Unlike Spring Boot's HandlerInterceptor, JAX-RS filters use abortWith() to block.
abortWith() — not return false
JAX-RS filters block by calling requestContext.abortWith(Response) (classic) or returning a Response object (RESTEasy Reactive). Unlike Spring's HandlerInterceptor.preHandle() (return false), you pass a fully built response object. This means you control the status code, body, and headers of the 403 response exactly.
Protection layers
Layer 1: robots.txt
Create the file at src/main/resources/META-INF/resources/robots.txt. Quarkus serves everything under META-INF/resources/ as static files at the root URL — no config needed:
# src/main/resources/META-INF/resources/robots.txt User-agent: * Allow: / User-agent: GPTBot User-agent: ClaudeBot User-agent: anthropic-ai User-agent: Google-Extended User-agent: CCBot User-agent: cohere-ai User-agent: Bytespider User-agent: Amazonbot User-agent: PerplexityBot User-agent: YouBot User-agent: Diffbot User-agent: DeepSeekBot User-agent: MistralBot User-agent: xAI-Bot User-agent: AI2Bot Disallow: /
src/main/resources/META-INF/resources/robots.txt → served at /robots.txt. src/main/resources/META-INF/resources/static/logo.png → served at /static/logo.png. Static files are served by Vert.x before JAX-RS filters run.RESTEasy Reactive — @ServerRequestFilter
For quarkus-resteasy-reactive (recommended for new projects)
Annotate a CDI bean method with @ServerRequestFilter(preMatching = true). Return a Response to block, return null to continue:
// src/main/java/com/example/filter/AiBotFilter.java
package com.example.filter;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Set;
@ApplicationScoped
public class AiBotFilter {
private static final List<String> AI_BOTS = List.of(
"gptbot", "chatgpt-user", "claudebot", "anthropic-ai",
"ccbot", "cohere-ai", "bytespider", "amazonbot",
"applebot-extended", "perplexitybot", "youbot", "diffbot",
"google-extended", "deepseekbot", "mistralbot", "xai-bot",
"ai2bot", "oai-searchbot", "duckassistbot"
);
private static final Set<String> EXEMPT_PATHS = Set.of(
"/robots.txt", "/sitemap.xml", "/favicon.ico", "/q/health"
);
@ServerRequestFilter(preMatching = true)
public Response filterRequest(ContainerRequestContext requestContext) {
String path = requestContext.getUriInfo().getPath();
// Exempt paths bypass blocking
if (EXEMPT_PATHS.contains(path)) {
return null; // continue
}
String ua = requestContext.getHeaderString("User-Agent");
if (ua == null) return null;
String uaLower = ua.toLowerCase();
for (String bot : AI_BOTS) {
if (uaLower.contains(bot)) {
return Response
.status(Response.Status.FORBIDDEN)
.entity("Forbidden: AI crawlers are not permitted.")
.build();
}
}
return null; // continue to resource method
}
@ServerResponseFilter
public void filterResponse(ContainerRequestContext requestContext,
jakarta.ws.rs.container.ContainerResponseContext responseContext) {
responseContext.getHeaders().add("X-Robots-Tag", "noai, noimageai");
}
}Classic JAX-RS — ContainerRequestFilter
For quarkus-resteasy (blocking, classic JAX-RS)
Implement ContainerRequestFilter, annotate with @Provider and @PreMatching, call requestContext.abortWith() to block:
// src/main/java/com/example/filter/AiBotFilter.java
package com.example.filter;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import java.io.IOException;
import java.util.List;
import java.util.Set;
@Provider
@PreMatching
@Priority(Priorities.AUTHENTICATION - 100) // 900 — runs before auth
public class AiBotFilter implements ContainerRequestFilter, ContainerResponseFilter {
private static final List<String> AI_BOTS = List.of(
"gptbot", "chatgpt-user", "claudebot", "anthropic-ai",
"ccbot", "cohere-ai", "bytespider", "amazonbot",
"applebot-extended", "perplexitybot", "youbot", "diffbot",
"google-extended", "deepseekbot", "mistralbot", "xai-bot",
"ai2bot"
);
private static final Set<String> EXEMPT_PATHS = Set.of(
"/robots.txt", "/sitemap.xml", "/favicon.ico"
);
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String path = requestContext.getUriInfo().getPath();
if (EXEMPT_PATHS.contains(path)) {
return; // continue
}
String ua = requestContext.getHeaderString("User-Agent");
if (ua == null) return;
String uaLower = ua.toLowerCase();
for (String bot : AI_BOTS) {
if (uaLower.contains(bot)) {
// abortWith() — resource method never runs
requestContext.abortWith(
Response.status(Response.Status.FORBIDDEN)
.entity("Forbidden: AI crawlers are not permitted.")
.build()
);
return;
}
}
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) throws IOException {
// X-Robots-Tag on every response
responseContext.getHeaders().add("X-Robots-Tag", "noai, noimageai");
}
}Filter priority
JAX-RS defines standard priority constants in jakarta.ws.rs.Priorities. Lower values run first:
// Reactive: priority on the method annotation
@ServerRequestFilter(preMatching = true, priority = Priorities.AUTHENTICATION - 100)
public Response filterRequest(ContainerRequestContext ctx) { ... }
// Classic: @Priority annotation on the class
@Provider
@PreMatching
@Priority(Priorities.AUTHENTICATION - 100)
public class AiBotFilter implements ContainerRequestFilter { ... }Quarkus vs Spring Boot vs Micronaut — blocking comparison
Quarkus RESTEasy Reactive — @ServerRequestFilter
@ServerRequestFilter(preMatching = true)
public Response filterRequest(ContainerRequestContext ctx) {
String ua = ctx.getHeaderString("User-Agent");
if (ua != null && isAiBot(ua.toLowerCase()))
return Response.status(403).entity("Forbidden").build();
return null; // continue
}Quarkus Classic JAX-RS — ContainerRequestFilter.abortWith()
@Override
public void filter(ContainerRequestContext ctx) {
String ua = ctx.getHeaderString("User-Agent");
if (ua != null && isAiBot(ua.toLowerCase()))
ctx.abortWith(Response.status(403).entity("Forbidden").build());
// return without abortWith = continue
}Spring Boot — OncePerRequestFilter
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain) throws ... {
String ua = req.getHeader("User-Agent");
if (ua != null && isAiBot(ua.toLowerCase())) {
res.sendError(403, "Forbidden"); return;
}
chain.doFilter(req, res); // continue
}Micronaut — HttpServerFilter.doFilter()
@Override
public Publisher<MutableHttpResponse<?>> doFilter(
HttpRequest<?> req, ServerFilterChain chain) {
String ua = req.getHeaders().get("User-Agent", "");
if (isAiBot(ua.toLowerCase()))
return Mono.just(HttpResponse.status(HttpStatus.FORBIDDEN));
return chain.proceed(req); // continue
}Testing
Use @QuarkusTest with RestAssured (included automatically):
// src/test/java/com/example/filter/AiBotFilterTest.java
package com.example.filter;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
class AiBotFilterTest {
@Test
void testBlocksAiBot() {
given()
.header("User-Agent", "GPTBot/1.0")
.when()
.get("/api/articles")
.then()
.statusCode(403);
}
@Test
void testAllowsBrowser() {
given()
.header("User-Agent", "Mozilla/5.0 (compatible)")
.when()
.get("/api/articles")
.then()
.statusCode(200)
.header("X-Robots-Tag", "noai, noimageai");
}
@Test
void testRobotsTxtAlwaysAccessible() {
given()
.header("User-Agent", "GPTBot/1.0")
.when()
.get("/robots.txt")
.then()
.statusCode(200);
}
}Run: ./mvnw test or ./gradlew test
AI bot User-Agent strings (2026)
Call .toLowerCase() on the UA string before matching — use String.contains() for substring match.
Is your site protected from AI bots?
Run a free scan to check your robots.txt, meta tags, and overall AI readiness score.