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) // covered5. 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
ctx.GetHeader()returns"", notnil: When theUser-Agentheader is absent,ctx.GetHeader()returns an empty string — safe to pass directly toisAiBot()without a nil check. This mirrors the underlyingnet/http Header.Get()behaviour.app.UseGlobal()vsapp.Use():app.Use()only covers routes registered after the call.app.UseGlobal()applies to all routes regardless of order. For bot blocking, preferUseGlobal()— a route accidentally registered beforeUse()will silently bypass the middleware with no error.- Set headers before
ctx.StopWithText(): Iris writes response headers on the first write to the body. Callingctx.Header()afterctx.StopWithText()is a no-op — theX-Robots-Tagheader will not appear in the response. Always set headers first. - Do not call
ctx.Next()afterctx.StopWithText():StopWithTexthalts the chain. CallingNext()afterwards re-enters the chain — the route handler will run and attempt to write a second response, causing a "response already sent" panic or silent data corruption. iris.Contextis an interface: Unlike Gin (*gin.Context) and Echo (echo.Contextinterface but typically a struct pointer), Iris middleware receives theiris.Contextinterface directly. The concrete implementation is managed by Iris internally — you always work against the interface in middleware.- Party middleware uses
Use(), notUseGlobal():UseGlobal()only exists on the rootiris.Application. On a Party, callparty.Use()— but register all party routes after theUse()call on the party, or the same registration-order rule applies.
Framework comparison — popular Go web frameworks
| Framework | Middleware signature | Block | UA header |
|---|---|---|---|
| Iris | func(ctx iris.Context) | ctx.StopWithText(403, "Forbidden") | ctx.GetHeader("User-Agent") |
| Gin | func(c *gin.Context) | c.AbortWithStatus(403); return | c.GetHeader("User-Agent") |
| Echo | func(next echo.HandlerFunc) echo.HandlerFunc | return echo.NewHTTPError(403, "Forbidden") | c.Request().Header.Get("User-Agent") |
| Fiber | func(c *fiber.Ctx) error | return 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.