Skip to content
Guides/Quarkus (Java)

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

1
robots.txtsrc/main/resources/META-INF/resources/robots.txt — auto-served by Quarkus at /robots.txt
2
noai meta tagInject robots value via CDI RequestScoped bean, read in Qute template
3
X-Robots-Tag header@ServerResponseFilter adds header to every outgoing response
4
Hard 403 blockabortWith(Response.status(403).build()) in @PreMatching filter — resource method never runs

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: /
META-INF/resources/ — Quarkus static root
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:

< 1000Your bot filtere.g. Priorities.AUTHENTICATION - 100 = 900
1000Priorities.AUTHENTICATIONauth token validation
2000Priorities.AUTHORIZATIONrole checks
3000Priorities.HEADER_DECORATORheader manipulation
4000Priorities.ENTITY_CODERencoding
5000Priorities.USERapplication filters
// 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)

GPTBotChatGPT-UserClaudeBotanthropic-aiCCBotcohere-aiBytespiderAmazonbotApplebot-ExtendedPerplexityBotYouBotDiffbotGoogle-ExtendedFacebookBotomgiliomgilibotDeepSeekBotMistralBotxAI-BotAI2Bot

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.