Skip to content

How to Block AI Bots in Go Iris

Iris is a high-performance Go web framework with its own iris.Context interface — a superset of net/http that provides shorthand helpers like ctx.GetHeader() and ctx.StopWithText(). Bot blocking uses app.Use() or app.UseGlobal() to register a global middleware handler. ctx.GetHeader() returns an empty string when the header is absent — no nil check needed. ctx.StopWithText(statusCode, body) writes the response and stops the chain; ctx.Next() passes through to the next handler. The key registration-order gotcha: app.Use() only applies to routes registered after it — app.UseGlobal() is order-independent.

1. Bot detection

Pure Go, no dependencies. strings.ToLower() + strings.Contains() — literal substring match, no regex. Early return on empty UA.

// botutils.go — AI bot detection, no external dependencies
package main

import "strings"

var aiBotPatterns = []string{
    "gptbot",
    "chatgpt-user",
    "claudebot",
    "anthropic-ai",
    "ccbot",
    "google-extended",
    "cohere-ai",
    "meta-externalagent",
    "bytespider",
    "omgili",
    "diffbot",
    "imagesiftbot",
    "magpie-crawler",
    "amazonbot",
    "dataprovider",
    "netcraft",
}

// isAiBot returns true if ua matches a known AI crawler pattern.
// strings.Contains() — literal substring match, no regex.
// strings.ToLower() normalises before comparison.
func isAiBot(ua string) bool {
    if ua == "" {
        return false
    }
    lower := strings.ToLower(ua)
    for _, pattern := range aiBotPatterns {
        if strings.Contains(lower, pattern) {
            return true
        }
    }
    return false
}

2. Middleware — func(ctx iris.Context)

Iris middleware is func(ctx iris.Context). ctx.GetHeader("User-Agent") returns "" when absent. ctx.StopWithText() writes the response body and stops the chain — do not call ctx.Next() after it. Set response headers before StopWithText — headers lock on first write.

// middleware.go — Iris bot-blocking middleware
package main

import "github.com/kataras/iris/v12"

// BotBlockerMiddleware is a global Iris middleware handler.
// iris.Context is an interface — the middleware signature is func(ctx iris.Context).
func BotBlockerMiddleware(ctx iris.Context) {
    // Path guard: robots.txt must be accessible so bots can read Disallow rules.
    if ctx.Path() == "/robots.txt" {
        ctx.Next() // continue to the robots.txt handler
        return
    }

    // ctx.GetHeader() returns "" when the header is absent — no nil check needed.
    // This is shorthand for ctx.Request().Header.Get("User-Agent").
    // net/http canonicalises header names to Title-Case internally.
    ua := ctx.GetHeader("User-Agent")

    if isAiBot(ua) {
        // Block: set headers BEFORE StopWithText — headers are locked after the
        // first write. StopWithText writes the body and stops the chain.
        // Do NOT call ctx.Next() after StopWithText.
        ctx.Header("X-Robots-Tag", "noai, noimageai")
        ctx.Header("Content-Type", "text/plain")
        ctx.StopWithText(iris.StatusForbidden, "Forbidden")
        return
    }

    // Pass: inject X-Robots-Tag on legitimate requests, then delegate.
    // ctx.Next() advances to the next middleware or the final route handler.
    ctx.Header("X-Robots-Tag", "noai, noimageai")
    ctx.Next()
}

3. main.go — UseGlobal registration

app.UseGlobal() applies the middleware to all routes regardless of when they were registered. iris.New() creates a bare app; iris.Default() adds Logger + Recovery middleware on top.

// main.go — Iris app with global bot-blocking middleware
package main

import "github.com/kataras/iris/v12"

func main() {
    app := iris.New() // iris.Default() also adds Logger + Recovery middleware

    // UseGlobal registers middleware for ALL routes, regardless of registration order.
    // app.Use() only applies to routes registered AFTER the Use() call.
    // For bot blocking, UseGlobal() is safer — route order doesn't matter.
    app.UseGlobal(BotBlockerMiddleware)

    // robots.txt — pass-through guard in the middleware lets bots reach this.
    app.Get("/robots.txt", func(ctx iris.Context) {
        ctx.Header("Content-Type", "text/plain")
        ctx.WriteString(`User-agent: *
Allow: /

User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: Google-Extended
Disallow: /
`)
    })

    app.Get("/", func(ctx iris.Context) {
        ctx.JSON(iris.Map{"message": "Hello"})
    })

    app.Get("/api/data", func(ctx iris.Context) {
        ctx.JSON(iris.Map{"data": "value"})
    })

    // Listen blocks until the process receives SIGINT or SIGTERM.
    app.Listen(":8080")
}

4. Use() vs UseGlobal() — registration order

This is the most common Iris gotcha. app.Use() only applies to routes registered after it. If a route is registered before Use(), the middleware silently does not run for that route. UseGlobal() is order-independent.

// USE vs USEGLOBAL — registration-order gotcha

app := iris.New()

// ❌ WRONG — Use() called AFTER route registration.
// The "/" route is NOT covered by the middleware.
app.Get("/", homeHandler)
app.Use(BotBlockerMiddleware) // too late for routes registered above

// ✅ CORRECT — UseGlobal() applies to all routes regardless of order.
app.Get("/", homeHandler)
app.UseGlobal(BotBlockerMiddleware) // covers all routes including "/" above

// ✅ ALSO CORRECT — Use() called BEFORE route registration.
app.Use(BotBlockerMiddleware)       // registered first
app.Get("/", homeHandler)           // covered

5. Scoped middleware — app.Party()

app.Party("/api") creates a route group with its own middleware stack. apiParty.Use() applies only to routes registered on the party — public routes on the root app are unaffected. Equivalent to Gin's router.Group() or Echo's e.Group().

// Scoped middleware — protect /api routes only using an Iris Party.
// A Party is a route group with its own middleware stack.
// Routes on the root app are NOT affected by party middleware.

package main

import "github.com/kataras/iris/v12"

func main() {
    app := iris.New()

    // Public routes — no bot blocking
    app.Get("/robots.txt", robotsHandler)
    app.Get("/", publicHandler)

    // Protected party — /api/** routes get the bot blocker
    // app.Party() returns an iris.Party (implements iris.APIBuilder)
    apiParty := app.Party("/api")
    apiParty.Use(BotBlockerMiddleware) // scoped to /api/**

    apiParty.Get("/data", dataHandler)
    apiParty.Get("/status", statusHandler)

    app.Listen(":8080")
}

6. go.mod

# go.mod — Iris v12 dependency
module example.com/botblocker

go 1.21

require (
    github.com/kataras/iris/v12 v12.2.11
)

# go get github.com/kataras/iris/v12@latest
# go run .

Key points

Framework comparison — popular Go web frameworks

FrameworkMiddleware signatureBlockUA header
Irisfunc(ctx iris.Context)ctx.StopWithText(403, "Forbidden")ctx.GetHeader("User-Agent")
Ginfunc(c *gin.Context)c.AbortWithStatus(403); returnc.GetHeader("User-Agent")
Echofunc(next echo.HandlerFunc) echo.HandlerFuncreturn echo.NewHTTPError(403, "Forbidden")c.Request().Header.Get("User-Agent")
Fiberfunc(c *fiber.Ctx) errorreturn c.Status(403).SendString("Forbidden")c.Get("User-Agent")

Iris and Gin share the ctx.GetHeader() shorthand and empty-string-on-missing semantics. The key divergence is blocking style: Iris uses StopWithText() (imperative stop), Gin uses AbortWithStatus() (imperative abort), Echo uses error return (functional), and Fiber uses error return with an immediate-send helper. Iris's UseGlobal() has no direct equivalent in Gin or Echo — both require middleware to be registered before routes.