Iris ships thirty-one middleware packages under
github.com/kataras/iris/v14/middleware/. Each one is a plain
iris.Handler (or a small type that produces one), so they compose with
your own handlers without adapters. This chapter is the tour: a catalog of
everything that exists, what the iris.NewBuilder chain already installs
for you, and a focused section with a working example for each middleware
you are likely to reach for first. Topics that deserve a whole chapter get
only a brief entry here with a pointer: authentication in
Chapter 10, caching strategies in
Chapter 14, i18n in
Chapter 15 and observability in
Chapter 17.
Background
Middleware, introduced in Chapter 2, is a handler
that runs before or around your route handler. Every request flows
through a chain: each middleware can inspect the request, attach data for
later handlers, modify the response, pass control on with ctx.Next(),
or stop the chain entirely. Cross-cutting concerns (the jobs every route
needs but no route wants to implement) live naturally in this chain,
written once and registered wherever they apply.
Each middleware family in this chapter exists because of a concrete problem:
- CORS: browsers deliberately block a page on one site from calling an API on another until the API states, through response headers, which foreign origins it accepts.
- Rate limiting: any public endpoint eventually gets hammered (by brute-force login scripts, scrapers or a buggy client stuck in a retry loop), so requests per client must be throttled.
- Recovery: one panicking handler would otherwise tear down its request with a blank response; crash containment turns the panic into a controlled, logged 500.
- Compression and body limits: text responses shrink several-fold when compressed, saving bandwidth on every reply. In the other direction, a client can send an arbitrarily large body and exhaust your memory unless a cap rejects it early.
- Access logging: when something goes wrong in production, the audit trail (who requested what, when, and how it ended) is the record you reconstruct events from.
- Sessions and tokens: HTTP is stateless, the server forgets each client between requests, so remembering “who is this” takes deliberate machinery.
“Batteries included” means these tools ship inside the framework rather
than as third-party add-ons you assemble yourself. That saves more than
typing: every package here is versioned with Iris, tested against the
same context and router, and upgraded in lock-step, so the classic
integration risks disappear: an adapter written for an older API, two
libraries disagreeing about who writes headers first, a security default
one package sets and another silently undoes. You can still plug in
anything from the net/http ecosystem (see
Adapting net/http Middleware); you just
rarely need to.
The Middleware Catalog
Every package lives under github.com/kataras/iris/v14/middleware/<name>.
| Middleware | Package | Purpose | Constructor form |
|---|---|---|---|
| Access log | accesslog | Structured request/response logging | accesslog.File(path) or accesslog.New(w), then .Handler |
| API key | apikey | Shared-secret authentication for machine callers | apikey.New(apikey.Options{Keys: ...}) |
| Basic auth | basicauth | HTTP Basic Authentication | basicauth.Default(users), basicauth.New(Options[U]), basicauth.Load(file) |
| Body limit | bodylimit | Cap request body size (413 on excess) | bodylimit.New(2 * iris.MB) |
| Cache | cache | ETag, Cache-Control and friends | cache.New(cache.Options{...}) |
| Compress | compress | Response compression negotiation | compress.Handler |
| CORS | cors | Cross-Origin Resource Sharing | cors.New()....Handler() |
| Counter | counter | Request counting with notifications | counter.RequestCounter(counter.NewCounter[uint64]()) |
| Geolocation | geolocation | IP-based geolocation lookup | geolocation.New(geolocation.Options{...}) |
| gRPC | grpc | Serve a gRPC server through Iris | grpc.New(grpcServer) |
| hCaptcha | hcaptcha | hCaptcha challenge verification | hcaptcha.New(hcaptcha.Options{Secret: ...}) |
| HTTP cost | httpcost | Per-request cost (time, RAM, CPU) | httpcost.New(httpcost.DefaultOptions) |
| i18n | i18n | Translations and language detection | i18n.NewHandler(locales, i18n.HandlerOptions{...}) |
| IP access | ipaccess | IP allowlist and blocklist | ipaccess.New(allowed, blocked) |
| JWT | jwt | Token signing and verification | jwt.NewVerifier(alg, key).Verify(claims) |
| Method override | methodoverride | PUT/DELETE from HTML forms | methodoverride.New(methodoverride.Options{...}) |
| Mod revision | modrevision | Build and health info endpoint | modrevision.New(modrevision.Options{...}) |
| Monitor | monitor | Process and OS statistics page | monitor.New(monitor.Options{...}) |
| pprof | pprof | Go profiling endpoints | app.Router("/debug/pprof", pprof.New()) |
| Rate | rate | Token-bucket rate limiting | rate.Limit(limit, burst, rate.Options{...}) |
| reCAPTCHA | recaptcha | Google reCAPTCHA verification | recaptcha.New(recaptcha.Options{Secret: ...}) |
| Recorder | recorder | Record request/response bodies | recorder.Request handler, recorder.GetResponseRecorder(ctx) |
| Recovery | recovery | Panic recovery | recovery.Handler |
| Referrer | referrer | Referer header parsing | referrer.Handler |
| Request ID | requestid | Unique per-request identifiers | requestid.New(generator...) |
| Rewrite | rewrite | Redirect and rewrite rules | rewrite.New(rewrite.Options{...}), rewrite.Load(file) |
| Server timing | servertiming | Server-Timing response header | servertiming.Handler, then Start/End |
| Sessions | sessions | Typed server-side sessions | sessions.Handler(db, cookieName) |
| Versioning | versioning | API versioning by header/query/subdomain | versioning.New(versioning.Options{...}) |
| View | view | Template engine integration | view.Handler(engine), then view.Render |
| WebSocket | websocket | WebSockets via neffos | websocket.New(server, websocket.Options{}) |
One convention runs through that last column. A package with settings
exposes an Options struct whose zero value is valid, and a
New(opts Options) iris.Handler that takes it; a package with nothing to
configure exposes a plain Handler instead, so there is no empty
constructor to call. Where construction can fail, New returns
(handler, error) and a Must companion panics instead, for the common
case of wiring at start-up. The handful of packages that produce something
other than one handler, accesslog and cors among them, say so in the
first paragraph of their package documentation.
That is why there are no functional options anywhere here: no
rate.PurgeEvery(...), no methodoverride.HeadersOnly(). Everything they
used to set is a field on the package’s Options.
Registration Levels
Where you register a middleware decides which requests it sees:
app := iris.New()
// Root-level Use: every request, including 404 and 405 responses.
app.Use(handler)
// Child-level Use: only routes matched under this group.
api := app.Router("/api")
api.Use(handler)
// UseRouter: runs before routing itself — static files, rewrites,
// CORS preflights and everything else pass through it.
app.UseRouter(handler)As a rule of thumb: infrastructure that must see every byte (CORS,
compression, access logging, rewriting, versioning, recovery) belongs to
UseRouter; request-scoped behavior (rate limits, body limits, sessions,
authentication, cache headers) belongs to Use on the group that needs
it.
What the Builder Installs for You
If you assemble your application with iris.NewBuilder
(Chapter 3), several middleware from this chapter
are registered by the chain itself:
- Recovery is installed unless one is already registered:
recovery.WithErrorHandler(errors.Internal.LogErr)followed byrecovery.Handler, so panics become logged 500 INTERNAL responses. The builder skips it when a handler namediris.recoveryoriris.errors.recoveris already on the router. - CORS: the
AllowOrigin(originLine)step registerscors.New().HandleErrorFunc(errors.FailedPrecondition.Err). AllowOrigin(originLine).Handler(). TheCors(c)step at the same position takes a CORS value you built yourself, for the settings the origin line cannot reach; it is used as given, so setHandleErrorFuncto keep the structured error body. - Compression:
Compression(true)only records the choice; thecompress.Handleritself is registered later, by the firstRouterMiddlewaresorMiddlewarescall, and on the root rather than the prefix router. - Access logging:
LogRequests(true)registers a tunedaccesslog.AccessLog(JSON lines toaccess_log.jsonplus stdout) and closes it on shutdown;LogRequestsWith(ac)registers your own instance instead. - Health endpoint:
Health(true, env, developer)registersmodrevision.NewatGET /healthon the root, outside the prefix. - Rate exceed handler: the builder binds the
ratemiddleware’s 429 response to the JSON error format of therest/errorspackage.
Order matters here, and it is not the order of the list above. CORS and the
access log are registered by their own builder steps, so they are already
in the chain by the time the first RouterMiddlewares or Middlewares
call installs the rest. That is why recovery does not wrap them, and why
the access log sees the 500 that recovery produces:
With the builder, you only reach into middleware/ for what the chain
does not cover: rate limits, sessions, JWT, body limits, cache headers
and the rest of this chapter.
CORS
First, the mechanics. The browser’s same-origin policy stops
JavaScript served from https://app.example.com from reading responses
of https://api.example.com, a different origin (scheme, host and
port), unless the API opts in. CORS (Cross-Origin Resource Sharing) is
that opt-in: the server names acceptable origins in Access-Control-*
response headers. For requests that could have side effects, the browser
first sends a preflight (an automatic OPTIONS request asking “may
I send a POST with these headers?”) and proceeds only when the answer
allows it. CORS protects browser users; it does not restrict non-browser
clients such as curl, which ignore it entirely.
The cors middleware answers preflight requests and stamps the
Access-Control-* response headers. It is configured through a fluent
chain that ends in .Handler(), and it belongs to UseRouter so that
preflights are answered even for unmatched paths.
package main
import (
"time"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/middleware/cors"
)
func main() {
app := iris.New()
crs := cors.New().
// One comma-separated line; use AllowOrigins for a list.
AllowOrigin("https://example.com,https://app.example.com").
AllowHeaders("Content-Type", "Authorization").
ExposeHeaders("X-Total-Count").
MaxAge(12 * time.Hour).
Handler()
app.UseRouter(crs)
app.Get("/api/data", func(ctx iris.Context) {
ctx.JSON(iris.Map{"message": "cross-origin ready"})
})
app.Listen(":8080")
}For dynamic origin decisions, pick one of three validators:
// Match on the origin string alone.
cors.New().AllowOriginMatcherFunc(func(origin string) bool {
return strings.HasSuffix(origin, ".example.com")
}).Handler()
// Context-aware: AllowOriginFunc(func(ctx iris.Context,
// origin string) bool { ... })
// Regular expressions: AllowOriginRegex(`^https://.*\.example\.com$`)Other chain methods: AllowOrigins("https://a.com", "https://*.b.com")
for a list with wildcards, DisallowCredentials(),
ReferrerPolicy(cors.NoReferrerWhenDowngrade),
ExtractOriginFunc(cors.StrictOriginExtractor),
SkipLocalRequests(true) and HandleErrorFunc to customize the
rejection response. There is no AllowMethods method: a preflight’s
Access-Control-Allow-Methods always lists every standard method, QUERY
included, and route registration controls which verbs actually exist.
Rate Limiting
Picture each client owning a bucket of tokens: every request spends one token, and the bucket refills at a steady rate up to a maximum, the burst. A client that stays under the refill rate never notices the limiter; one that surges drains its bucket and is rejected until tokens accumulate again. This “token bucket” scheme tolerates short, normal bursts of activity while capping the sustained rate.
The rate middleware implements a token-bucket limiter per client.
rate.Limit(limit, burst, rate.Options{}) takes events per second as a
float; the rate.Every helper converts an interval, and the zero
rate.Options keeps every default.
package main
import (
"time"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/middleware/rate"
"github.com/kataras/iris/v14/rest/errors"
)
func main() {
app := iris.New()
// 1 request per second, bursts of up to 5.
app.Use(rate.Limit(1, 5, rate.Options{}))
// A stricter group: 10 per minute, with periodic cleanup of
// clients idle for more than 5 minutes and a custom response.
strict := rate.Limit(rate.Every(time.Minute), 10, rate.Options{
PurgeEvery: 5 * time.Second,
PurgeMaxLifetime: 5 * time.Minute,
ExceedHandler: func(ctx iris.Context) {
ctx.SetHeader("Retry-After", "60")
errors.ResourceExhausted.Message(ctx, "rate limit exceeded")
},
})
auth := app.Router("/auth", strict)
auth.Post("/login", func(ctx iris.Context) {
ctx.JSON(iris.Map{"ok": true})
})
app.Listen(":8080")
}By default clients are identified by remote IP and rejected with a plain
429. To key on something else (an API key, a user ID), call
rate.SetIdentifier from a middleware registered before the limiter:
app.Use(func(ctx iris.Context) {
rate.SetIdentifier(ctx, ctx.Header("X-API-Key"))
ctx.Next()
})
app.Use(rate.Limit(1, 5, rate.Options{}))Inside handlers, rate.Get(ctx) returns the *rate.Client (its ID,
optional Data filled by the Options.ClientData hook, and the
underlying limiter) for emitting X-RateLimit-* headers. The limiter
state is in-process memory; limits shared across replicas need a proxy
or a store-backed limiter in front.
rate.Limit returns a handler and manages the limiter’s lifetime for
you. When you need the lifetime in your hands, for example to stop the
purge goroutine of a limiter that lives shorter than the process,
rate.New(limit, burst, rate.Options{}) returns the *rate.Limiter itself:
register its Handler() and call Stop() when the limiter is no
longer needed.
Recovery
The recovery middleware turns panics into 500 responses instead of
crashed connections. Register it with UseRouter so it wraps everything
that follows.
import "github.com/kataras/iris/v14/middleware/recovery"
app.UseRouter(recovery.Handler)
app.Get("/boom", func(ctx iris.Context) {
panic("something went wrong")
})To control the response, register recovery.WithErrorHandler before
recovery.Handler:
app.UseRouter(
recovery.WithErrorHandler(func(ctx iris.Context, err error) {
// Logs the panic dump, answers the canonical INTERNAL body.
errors.Internal.LogErr(ctx, err)
}),
recovery.Handler,
)The error passed to the handler is a *context.ErrPanicRecovery with
Cause, Stack, Callers, CurrentHandlerName,
CurrentHandlerFileLine and the Request. The Request dump has its
credential-bearing headers replaced with [redacted], the list being
recovery.RedactedRequestHeaders, and Error() reports only the cause
and the callers. Call LogMessage() when you want the dump as well, so
that a handler formatting the error cannot put request headers in a
response by accident. Builder-built applications
already have recovery installed and wired to the central error map
(errors.Internal.LogErr), so panics come back as JSON
INTERNAL errors; see Chapter 9.
Compression
The compress middleware negotiates a content encoding from the
client’s Accept-Encoding header (gzip, deflate, brotli, snappy, s2
or zstd) and wraps the response writer. It is a single handler:
import "github.com/kataras/iris/v14/middleware/compress"
app.UseRouter(compress.Handler)That is the whole integration. Responses are compressed only when the
client advertises support; already-encoded responses pass through. With
the builder, Compression(true) registers exactly this handler. For
manual control the package also exposes
compress.GetAcceptEncoding(r, offers...) and writer helpers, but for
nearly every application the one-liner is correct.
Access Logging
The accesslog middleware writes one record per request with latency,
status, sizes and any custom fields. Create it once, register its
Handler with UseRouter, and close it on shutdown.
package main
import (
"os"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/middleware/accesslog"
)
func main() {
app := iris.New()
ac := accesslog.File("access_log.json")
defer ac.Close()
ac.AddOutput(os.Stdout)
ac.SetFormatter(&accesslog.JSON{Indent: " ", HumanTime: true})
ac.IP = true
ac.RequestBody = true
ac.ResponseBody = false
ac.AddFields(func(ctx iris.Context, f *accesslog.Fields) {
f.Set("request_id", ctx.ID())
})
app.UseRouter(ac.Handler)
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{"hello": "world"})
})
app.Listen(":8080")
}accesslog.New(w io.Writer) writes to any writer,
accesslog.File(path) opens a buffered file, and
accesslog.FileUnbuffered(path) trades throughput for immediate writes.
Formatters include &accesslog.JSON{}, &accesslog.CSV{} and
&accesslog.Template{Text: "..."}.
With the builder, LogRequests(true) installs a pre-tuned instance
(JSON lines to access_log.json plus stdout, IP and body logging on)
and registers its Close as a shutdown closer. The builder puts the
access log outside compression, so that a panic is still logged, which
means a compressed response reaches it as compressed bytes. It leaves
the response body out of any line whose response carries a
Content-Encoding other than identity, and does not call Redact for
it; the status, the byte counts and every other field are logged as
usual. A browser asks for compression on every request, so with
Compression(true) expect response bodies in the log only for clients
that did not. When the defaults do not fit, build your own and pass it
to LogRequestsWith: same slot in the chain, your configuration:
ac := accesslog.File("access_log.json")
ac.SetFormatter(&accesslog.JSON{HumanTime: true})
ac.ResponseBody = false
app := iris.NewBuilder().
Prefix("/api").
AllowOrigin("*").
Compression(true).
LogRequestsWith(ac). // instead of LogRequests(true)
Health(true, "dev", "you@example.com").
// Errors, Services, middlewares, API — see Chapter 3.
Build()Body Limits
The bodylimit middleware rejects a request whose body exceeds a byte
limit with a 413. A request that declares a larger Content-Length is
refused before your handler runs; one with no declared length fails as
soon as reading it passes the limit. Under errors.Handler both answer
413 TOO_LARGE with the message “request body too large”.
import "github.com/kataras/iris/v14/middleware/bodylimit"
// Global cap.
app.Use(bodylimit.New(2 * iris.MB))
// A larger cap only where uploads are expected.
uploads := app.Router("/uploads", bodylimit.New(100*iris.MB))The iris.KB and iris.MB constants keep the sizes readable. This
middleware complements (not replaces) the
ContextOptions.BodyMaxMemory option, which caps what the body-reading
helpers (ctx.Body, ctx.BindJSON and friends) will buffer; see
Chapter 5.
Cache Headers
The cache middleware sets HTTP caching response headers (ETag,
Cache-Control, Last-Modified, Expires, Pragma, Vary,
Proxy-Authenticate) and answers conditional requests
(If-None-Match, If-Modified-Since) with 304 Not Modified. It does
not store responses; it teaches clients and proxies to.
An ETag is a short fingerprint of a response (a hash, in essence)
sent alongside the body. On the next visit the client asks “has it
changed?” by echoing that fingerprint in If-None-Match; if the
resource is unchanged the server answers 304 Not Modified, a few bytes
instead of the full body. Cache-Control works one step earlier: it
tells the client how long it may reuse its stored copy without asking at
all.
package main
import (
"time"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/middleware/cache"
)
func main() {
app := iris.New()
public := cache.New(cache.Options{
ETag: true,
CacheControl: cache.NewCacheControlBuilder().
Public().
MaxAge(time.Hour). // browsers: 1 hour
SMaxAge(2 * time.Hour). // shared caches: 2 hours
MustRevalidate().
Build(),
LastModified: true,
Expires: 1 * time.Hour,
Vary: cache.NewVaryBuilder().AcceptEncoding().Build(),
})
app.Get("/catalog", public, func(ctx iris.Context) {
ctx.JSON(iris.Map{"items": []string{"a", "b", "c"}})
})
app.Listen(":8080")
}cache.DefaultOptions is a sensible public-caching preset
(ETag, one-hour max-age, two-hour s-maxage, 24-hour Expires,
Vary: Accept-Encoding), usable as cache.New(cache.DefaultOptions).
The CacheControlBuilder also offers Private(), NoCache(),
NoStore() and ProxyRevalidate(); the VaryBuilder offers Accept(),
AcceptLanguage(), UserAgent() and Add(...). Server-side response
caching and store-backed strategies are the subject of
Chapter 14.
Sessions
A cookie is a small piece of data the server asks the browser to store and send back with every later request, the standard way to recognize a returning visitor over stateless HTTP. A session builds on that: the cookie carries either the session data itself (signed and encrypted, as in the cookie backend below) or just an ID pointing at data stored server-side. Signing makes tampering detectable; encryption keeps the contents unreadable to the client.
The sessions middleware provides typed, generic session data: you
declare a struct, pick a backend, and get compile-time-safe reads and
writes. The defaults are secure: cookies are signed and encrypted,
Secure is on and SameSite is Lax.
package main
import (
"os"
"time"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/middleware/sessions"
)
type UserSession struct {
ID string
Username string
}
func main() {
app := iris.New()
// 64-byte hash key, 32-byte block key: the payload is signed
// and encrypted. Load them from configuration, never hardcode.
opts := sessions.NewOptions(
[]byte(os.Getenv("SESSION_HASH_KEY")),
[]byte(os.Getenv("SESSION_BLOCK_KEY")),
)
opts.MaxAge = 1 * time.Hour // default is 24 hours
// Defaults: Secure=true (HTTPS only), SameSite=Lax, Path="/".
// For plain-HTTP local development only: opts.Secure = false
db := sessions.NewCookieDatabase[UserSession](opts)
// Loads the session per request, auto-commits at the end.
app.Use(sessions.Handler(db, "my_session"))
app.Post("/login", func(ctx iris.Context) {
sess := sessions.Get[UserSession](ctx)
sess.Set(UserSession{ID: "1", Username: "makis"})
ctx.JSON(iris.Map{"ok": true})
})
app.Get("/me", func(ctx iris.Context) {
sess := sessions.Get[UserSession](ctx)
ctx.JSON(sess.Data)
})
app.Listen(":8080")
}Useful session methods: SetFlash and the generic GetFlash for
one-time messages; notice, ok := sess.GetFlash[string]("notice")
returns the typed value and removes it, while a message of a different
type stays in place with (zero, false), so a mistyped read is never
destructive (use any as the type argument for the old untyped
behavior). SetExtra and GetExtra[V]() carry data beside the typed
Data struct; the promoted Extra field remains readable directly
where the raw any is wanted. Then Delete(ctx) to expire the
session, RegenerateID(ctx) to rotate the server-side ID after
sign-in, and IsNew. sessions.ReadOnlyHandler loads without
auto-commit, and sessions.Single binds a database to a fixed cookie
name for use without the middleware.
Beyond the cookie backend,
sessions.NewPostgresDatabase[T](table, conn, opts) and
sessions.NewRedisDatabase[T](client, opts) store the data server-side.
How sessions combine with authentication flows is covered in
Chapter 10.
JWT
A JSON Web Token is a self-contained credential: a small JSON payload (the claims: who the user is, when the token expires) signed by the server. The signature lets the server trust an incoming token without a database lookup, because any tampering breaks verification. The flip side is that a stolen token works until it expires, which is why lifetimes are kept short and a blocklist enables revocation.
The jwt middleware signs and verifies JSON Web Tokens. In v14 the
verifier reads the token from the Authorization: Bearer <token> header
only; query-string extraction is opt-in, because URLs leak into logs.
package main
import (
"os"
"time"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/middleware/jwt"
"github.com/kataras/iris/v14/rest/errors"
)
type UserClaims struct {
jwt.Claims
Username string `json:"username"`
}
func main() {
app := iris.New()
secret := []byte(os.Getenv("JWT_SECRET"))
signer := jwt.NewSigner(jwt.HS256, secret, 15*time.Minute)
verifier := jwt.NewVerifier(jwt.HS256, secret)
verifier.WithDefaultBlocklist() // enables token revocation
app.Post("/token", func(ctx iris.Context) {
token, err := signer.Sign(&UserClaims{
Claims: jwt.Claims{Subject: "user-123"},
Username: "makis",
})
if errors.HandleError(ctx, err) {
return
}
ctx.OK(iris.Map{"token": string(token)}, nil)
})
protected := app.Router("/api",
verifier.Verify(func() any { return new(UserClaims) }))
protected.Get("/me", func(ctx iris.Context) {
claims, _ := jwt.Get[*UserClaims](ctx)
ctx.JSON(iris.Map{"username": claims.Username})
})
app.Listen(":8080")
}verifier.WithQueryToken() additionally accepts ?token=... for cases
like browser file downloads that cannot set a header. Supported
algorithms include HS256, HS384, HS512, RS256 and ES256. For
complete login flows, refresh tokens and the higher-level auth
package, see Chapter 10.
Basic Authentication
HTTP Basic Authentication is the protocol’s built-in login scheme: the
client sends username:password with every request, base64-encoded but
not encrypted, so it is only acceptable over HTTPS.
The basicauth middleware guards routes with HTTP Basic Authentication.
Three constructors cover the usual cases:
import "github.com/kataras/iris/v14/middleware/basicauth"
// Quick: a static user map.
guard := basicauth.Default(map[string]string{"admin": "secret"})
// Configured: realm, user source, re-ask interval. Options[U] names the
// user type the handlers read back with basicauth.GetUser[U](ctx).
guard = basicauth.New(basicauth.Options[basicauth.Map]{
Realm: "Admin Area",
Allow: basicauth.AllowUsersFile[basicauth.Map]("users.yml"),
MaxAge: 5 * time.Minute,
})
// Shortcut: users straight from a JSON or YAML file.
guard = basicauth.Load("users.yml")
admin := app.Router("/admin", guard)Default stores a *iris.SimpleUser, Load a basicauth.Map, and
basicauth.AllowUsers(list) with a []YourUser stores your own type;
ctx.User() sees each of them as a context.User. Suitable for internal
dashboards and tooling; for user-facing authentication prefer sessions or
JWT.
API Keys
basicauth and jwt both assume a person. The apikey middleware covers
the caller that is not one: another service, a cron job, a webhook sender
or a CLI tool, presenting one long random string it was handed once.
import "github.com/kataras/iris/v14/middleware/apikey"
guard := apikey.New(apikey.Options{
Keys: map[string]string{
"reporting": os.Getenv("REPORTING_API_KEY"),
},
})
api := app.Router("/api", guard)The key arrives as Authorization: Bearer <key> by default, with the
scheme matched case-insensitively per RFC 7235; set
Scheme: apikey.NoScheme for a bare value on a header such as
X-API-Key. Keys maps the principal name to its secret, so one key per
consumer makes a leak traceable, and apikey.Get(ctx) names the caller
that matched. When nothing else has set a user, a context.SimpleUser
carrying that principal is stored too, so ctx.User() works.
The default is to deny: no Keys and no Lookup rejects every request
and logs a warning on the first one. Hash: apikey.SHA256 lets Keys
hold digests instead of working credentials, and Lookup resolves keys
that live in a database, where an error is a 500 rather than a 401.
Options.Query reads the key from a query parameter, which the access log
then records with every other query argument, so mask it in a custom
formatter first. Chapter 10 covers
all of it.
Request IDs
The requestid middleware assigns each request a unique ID, exposes it
through requestid.Get(ctx) and ctx.ID(), and reflects it in the
X-Request-Id response header, the anchor for tracing a request across
logs.
import "github.com/kataras/iris/v14/middleware/requestid"
app.Use(requestid.New())
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{"request_id": requestid.Get(ctx)})
})The default generator honors a client-supplied X-Request-Id header and
generates a UUIDv4 only when it is absent. That is convenient behind a
trusted proxy that assigns IDs, but it lets arbitrary clients choose the
value that lands in your logs. When IDs must be unforgeable, use the
server-only generator:
app.Use(requestid.New(requestid.ServerOnlyGenerator))requestid.HashGenerator(includeBody) derives a fixed-length ID from
the request hash instead (not unique across identical requests).
Internationalization
The i18n middleware loads translation files and detects the request
language from a URL parameter, cookie, subdomain or the
Accept-Language header.
import "github.com/kataras/iris/v14/middleware/i18n"
locales := i18n.New()
if err := locales.Load("./locales/*/*", "en-US", "el-GR"); err != nil {
panic(err)
}
locales.SetDefault("en-US")
app.Use(i18n.NewHandler(locales, i18n.HandlerOptions{
URLParameter: "lang", // ?lang=el-GR
Cookie: "lang",
}))
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{
"message": i18n.GetMessage(ctx, "welcome", "Iris"),
"locale": i18n.GetLocale(ctx).Language(),
})
})i18n.SetLanguage(ctx, code) switches the language for the current
visitor, and i18n.NewRouter serves path-prefixed locales
(/en-US/...). Translation file formats, pluralization and template
integration are covered in
Chapter 15.
Views
The view middleware connects any template engine that implements a
single-method interface:
type Engine interface {
ExecuteTemplate(w io.Writer, templateName string,
layoutName string, data any) error
}Register the engine once, render from handlers:
import (
"github.com/kataras/iris/v14/middleware/view"
"github.com/kataras/blocks"
)
templates := blocks.New("./views")
if err := templates.Load(); err != nil {
panic(err)
}
app.UseRouter(view.Handler(templates))
app.Get("/", func(ctx iris.Context) {
view.Render(ctx, "index", "", iris.Map{"Title": "Home"})
})Helpers on the package set per-request state: view.Layout,
view.Data, view.KeyValue and view.AddFunc. Response rendering in
full (including content negotiation) is in
Chapter 6.
WebSockets
A WebSocket is a persistent, two-way connection between browser and server that begins as an ordinary HTTP request and then upgrades. Unlike the request/response cycle, either side can push messages at any moment, which is what chat, live dashboards and multiplayer features need.
The websocket middleware integrates the neffos WebSocket library.
import (
"github.com/kataras/iris/v14/middleware/websocket"
"github.com/kataras/neffos"
"github.com/kataras/neffos/gorilla"
)
events := neffos.Namespaces{
"default": neffos.Events{
"chat": func(c *neffos.NSConn, msg neffos.Message) error {
c.BroadcastOthers(msg)
return nil
},
},
}
server := neffos.New(gorilla.DefaultUpgrader, events)
app.Any("/ws", websocket.New(server, websocket.Options{}))websocket.GetContext(conn) recovers the originating iris.Context
inside event callbacks, useful for reading authentication state set by
earlier middleware. neffos itself (namespaces, rooms, client libraries)
is a third-party library with its own documentation.
Monitoring and Profiling
Four packages cover observability basics; the full story (metrics, tracing, dashboards) is Chapter 17.
monitor renders live process and OS statistics (CPU, RAM, connections, load):
import "github.com/kataras/iris/v14/middleware/monitor"
m := monitor.New(monitor.Options{ViewTitle: "My App"})
app.Get("/monitor", m.View) // HTML dashboard
app.Get("/monitor/stats", m.Stats) // JSONhttpcost measures what each request costs in elapsed time, memory and CPU, using configurable per-unit prices:
import "github.com/kataras/iris/v14/middleware/httpcost"
app.Use(httpcost.New(httpcost.DefaultOptions))
app.Get("/report", func(ctx iris.Context) {
// In a later middleware or accesslog field:
if metric, ok := httpcost.Get(ctx); ok {
_ = metric // metric.Cost and the raw measurements
}
ctx.JSON(iris.Map{"ok": true})
})pprof mounts the standard Go profiling endpoints as a routing controller:
import "github.com/kataras/iris/v14/middleware/pprof"
app.Router("/debug/pprof", pprof.New())
// then: go tool pprof http://localhost:8080/debug/pprof/profileGuard it (behind ipaccess, basicauth or a debug-only conditional)
before it reaches production.
servertiming emits the Server-Timing response header so browser
devtools show your server-side phases:
import "github.com/kataras/iris/v14/middleware/servertiming"
app.Use(servertiming.Handler)
app.Get("/api/data", func(ctx iris.Context) {
servertiming.Start(ctx, "db", "Database Query")
// ... query ...
servertiming.End(ctx, "db")
ctx.JSON(iris.Map{"data": "result"})
})Utility Middleware
Smaller tools, each a one- or two-line integration.
methodoverride lets HTML forms issue PUT and DELETE through POST,
via headers (X-HTTP-Method-Override and friends) or a _method form
field. The target may be any standard method, QUERY included:
import "github.com/kataras/iris/v14/middleware/methodoverride"
app.UseRouter(methodoverride.New(methodoverride.Options{
// Defaults: override POST only, standard headers, "_method" field.
SaveOriginalMethodContextKey: "_originalMethod",
}))
// ctx.Method() then reflects the overridden verb.rewrite applies redirect rules in "CODE PATTERN TARGET" form:
import "github.com/kataras/iris/v14/middleware/rewrite"
engine, err := rewrite.New(rewrite.Options{
RedirectMatch: []string{
"301 /old-api/(.*) /api/v1/$1",
},
PrimarySubdomain: "www", // redirect root domain to www.
})
if err != nil {
panic(err)
}
app.UseRouter(engine.Handler)
// Or from a YAML/JSON file: app.UseRouter(rewrite.Load("redirects.yml"))ipaccess restricts by IP or CIDR, with optional exceptions:
import "github.com/kataras/iris/v14/middleware/ipaccess"
guard, err := ipaccess.New(
[]string{"127.0.0.1", "10.0.0.0/8"}, // allowed
[]string{"10.0.5.9"}, // blocked exceptions
)
if err != nil {
panic(err)
}
admin := app.Router("/admin", guard)versioning routes requests to versioned route trees based on a header, query parameter or subdomain:
import "github.com/kataras/iris/v14/middleware/versioning"
app.UseRouter(versioning.New(versioning.Options{
Default: "1.0.0",
Latest: "2.0.0",
Header: "Accept-Version",
QueryParam: "v",
Versions: versioning.Versions{
{Range: ">=1.0.0 <2.0.0", Path: "/v1", Deprecated: true},
{Range: ">=2.0.0", Path: "/v2"},
},
}))Deprecated matches receive X-Api-Warn and deprecation headers.
modrevision serves build metadata (module version, revision, build
time, environment); the builder’s Health step uses it for
GET /health:
import "github.com/kataras/iris/v14/middleware/modrevision"
app.Get("/health", modrevision.New(modrevision.Options{
ServerName: "My API",
Env: "production",
Developer: "team@example.com",
CloudDetection: true,
}))counter counts requests with a generic, notification-capable counter:
import "github.com/kataras/iris/v14/middleware/counter"
c := counter.NewCounter[uint64]()
app.Use(counter.RequestCounter(c))
app.Get("/stats", func(ctx iris.Context) {
ctx.JSON(iris.Map{"total_requests": c.Get()})
})referrer parses the Referer header into its parts, so a landing
route can record where a visitor arrived from without a third-party
script:
import "github.com/kataras/iris/v14/middleware/referrer"
app.Use(referrer.Handler)
app.Get("/landing", func(ctx iris.Context) {
r := referrer.Get(ctx)
if r.Type == referrer.ReferrerSearch {
// Label is the engine ("Google", "Bing"); Query is the search text
// when the engine still sends it, which most no longer do.
ctx.JSON(iris.Map{"engine": r.Label, "query": r.Query})
return
}
ctx.JSON(iris.Map{"type": r.Type.String(), "domain": r.Domain})
})Type is one of indirect, direct, email, search or social. The header is
client-supplied and often absent, so treat it as a hint for analytics and
never as proof of where a request came from.
Auditing Requests and Responses
recorder keeps a copy of the request body and of the response body, so
a later handler can read either after the handler that produced it has
finished. Register recorder.Request, recorder.Response, or both.
Ordinarily the request body is drained by the first reader, and the response goes straight to the socket. That is what makes an audit trail awkward to write by hand, and what this middleware solves:
import "github.com/kataras/iris/v14/middleware/recorder"
// Runs after the handler: call Next first, then read what was recorded.
func auditTrail(ctx iris.Context) {
ctx.Next()
entry := iris.Map{
"request_id": ctx.ID(),
"path": ctx.Path(),
"request": string(recorder.ReadRequestBody(ctx)),
}
if rec := recorder.GetResponseRecorder(ctx); rec != nil {
entry["status"] = rec.Code
entry["response"] = rec.Body.String()
}
auditLog.Write(entry)
}
orders := app.Router("/admin/orders",
recorder.Request, recorder.Response, auditTrail)
orders.Post("/", createOrder)recorder.GetResponse(ctx) returns the same thing as an *http.Response
when that shape is more convenient.
Two costs are worth stating plainly. Recording buffers the whole body in
memory, so registering it globally turns every large upload and every file
download into an allocation of the same size; put it on the routes that
need the trail and nowhere else. And the recorded body is the real one,
passwords and card numbers included, so scrub it before it reaches a log
the way accesslog’s Redact does.
Geolocation
geolocation resolves the client IP to a country, region, city and
timezone. The common production use is picking a sensible default before
the visitor has chosen anything: a currency, a language, a nearest region.
import (
stderrors "errors" // "errors" here is the framework's rest/errors.
"github.com/kataras/iris/v14/middleware/geolocation"
"github.com/kataras/iris/v14/rest/errors"
)
app.Use(geolocation.New(geolocation.Options{
APIKey: os.Getenv("IP_API_KEY"),
Cache: geolocation.NewMemoryCache(10_000),
DisableLanguage: true, // skip the second lookup when you do not need it.
}))
app.Get("/pricing", func(ctx iris.Context) {
info, err := geolocation.Fetch(ctx)
if err != nil {
if stderrors.Is(err, geolocation.Err) {
ctx.OK(iris.Map{"currency": "USD"}, nil) // fall back, do not fail.
return
}
errors.HandleError(ctx, err)
return
}
ctx.OK(iris.Map{"country": info.CountryCode, "city": info.City}, nil)
})geolocation.Get(ctx) returns the already-resolved *Info without
fetching, for handlers that run after the middleware.
The lookup is a network call to an external service, so two rules follow. Configure the cache, or a burst of traffic from one office becomes a burst of identical API calls. And write the fallback branch first, because the service will be slow or down at some point and a pricing page that fails closed on a geo lookup is worse than one that shows dollars.
Geolocation is a guess about a network address, not a fact about a person. It answers “which currency should this page default to”, never “is this request allowed”. A VPN defeats it in one click.
Captcha Verification
hcaptcha and recaptcha verify a challenge token with the provider
before the handler runs. Register either as a route middleware on the
endpoint that accepts the form:
import "github.com/kataras/iris/v14/middleware/hcaptcha"
app.Post("/contact",
hcaptcha.New(hcaptcha.Options{Secret: os.Getenv("HCAPTCHA_SECRET")}),
func(ctx iris.Context) {
// Only reached when the challenge passed.
res, _ := hcaptcha.Get(ctx)
ctx.Application().Logger().Infof("solved for %s", res.Hostname)
submitTicket(ctx)
})recaptcha works the same way. Both take an Options value whose
FailureHandler field replaces the default rejection response, a 429 with
no body; recaptcha.DefaultFailureHandler and
hcaptcha.DefaultFailureHandler are the ones it replaces:
import (
"github.com/kataras/iris/v14/middleware/recaptcha"
"github.com/kataras/iris/v14/rest/errors"
)
app.Post("/signup", recaptcha.New(recaptcha.Options{
Secret: os.Getenv("RECAPTCHA_SECRET"),
FailureHandler: func(ctx iris.Context) {
errors.PermissionDenied.Message(ctx, "captcha verification failed")
},
}), signup)Both packages can render the client-side form for you during development:
hcaptcha.RenderForm(ctx, siteKey, "/contact") and
recaptcha.GetFormHTML(siteKey, "/signup"). Real applications usually
render their own markup and keep only the verification here.
The secret is a server-side credential and belongs in the environment, not
in the source or in the template beside the site key. Verification adds a
round trip to the provider on every submission, bounded by
hcaptcha.VerifyTimeout (10 seconds by default), so a captcha in front of
a hot path costs latency on every request that reaches it.
Serving gRPC on the Same Port
grpc lets one Iris application answer both gRPC and ordinary HTTP on a
single port, which matters when you have exactly one TLS port to spend:
import (
grpcWrapper "github.com/kataras/iris/v14/middleware/grpc"
"google.golang.org/grpc"
)
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, &greeterService{})
app := iris.New()
app.UseRouter(grpcWrapper.New(grpcServer))
// Ordinary routes are unaffected; gRPC traffic never reaches them.
app.Get("/health", healthHandler)
app.Run(iris.ServerOptions{
Addr: ":443",
CertFile: "cert.pem",
KeyFile: "key.pem",
})The wrapper inspects each request and forwards it to the gRPC server only
when it is a gRPC call, which it decides from HTTP/2 plus the
application/grpc content type. Everything else calls ctx.Next() and
continues to the router.
That check is also the constraint: gRPC requires HTTP/2, and Go’s HTTP/2
support is only enabled over TLS. Serve this over plaintext and the gRPC
half silently never matches, because the requests arrive as HTTP/1.1. Use
Run with a certificate, as above, rather than Listen.
Adapting net/http Middleware
Anything written for net/http converts with iris.FromStd:
// Supported forms:
// iris.FromStd(h http.Handler)
// iris.FromStd(func(w http.ResponseWriter, r *http.Request))
// iris.FromStd(func(w http.ResponseWriter, r *http.Request,
// next http.HandlerFunc))
app.Use(iris.FromStd(stdMiddleware))The converted handler sees the route the way a standard one expects. In
all three forms r.PathValue("id") returns the route parameter, and
r.Pattern holds the matched route’s template as you registered it, for
example /users/{id:uuid}, so standard middleware that labels metrics
or traces by route works unchanged. Register that middleware with Use,
not UseRouter: the pre-routing slot runs before a route is matched, so
there r.Pattern is empty, as it is in the built-in 404 and 405
handlers.
And any handler chain can run conditionally with
iris.NewConditionalHandler:
app.Use(iris.NewConditionalHandler(func(ctx iris.Context) bool {
return ctx.Header("X-Debug") == "1"
}, debugHeadersMiddleware))Community Middleware
Thirteen more middleware live outside the framework, in iris-contrib/middleware. They cover the integrations that would drag a vendor SDK into the framework’s dependency tree if they shipped here:
| Middleware | What it adds |
|---|---|
casbin | Authorization with ACL, RBAC and ABAC models |
cloudwatch | AWS CloudWatch metrics |
csrf | Cross-site request forgery tokens |
expmetric | Request counters exposed through expvar |
newrelic | The official New Relic Go agent |
pg | PostgreSQL integration |
prometheus | A Prometheus metrics endpoint |
secure | A set of common security headers |
throttler, tollboothic | Two other rate-limiting approaches |
zap | Request logging through go.uber.org/zap |
Each is its own Go module, so you install only the one you want and the rest of its dependency tree stays out of your build:
$ go get github.com/iris-contrib/middleware/casbin@masterSentry maintains its own Iris handler in getsentry/sentry-go rather than here.
Two entries in that repository, cors and jwt, predate the framework’s
own. Use the built-in
middleware/cors and middleware/jwt instead: they are
maintained alongside the framework and the builder wires both for you.
The main branch of that repository targets v14. The v12 line is kept
unchanged at its v12 tag, so an application that has not migrated yet
still has something to pin to.
These modules are maintained by their contributors rather than with the framework, so they move on their own schedule. Check when one was last touched before you build something load-bearing on it.
Summary
- Thirty-one middleware packages live under
github.com/kataras/iris/v14/middleware/; each is a plain handler or a small constructor that returns one. - Registration level matters:
UseRouterfor infrastructure that must see every request (CORS, compression, access logs, rewrites, recovery),Usefor request-scoped behavior (rate and body limits, sessions, authentication, cache headers). iris.NewBuilderalready installs recovery, and (through its chain steps) CORS, compression, access logging and the/healthendpoint, all wired to therest/errorsJSON error format.- The heavy hitters each take a handful of lines:
cors.New()... .Handler(),rate.Limit(limit, burst, rate.Options{}),compress.Handler,accesslog.File(path),bodylimit.New(n),cache.New(cache.Options{...})with itsCacheControlBuilder, typedsessionswith secure cookie defaults, ajwtverifier that reads theAuthorizationheader by default, andapikey.New(apikey.Options{Keys: ...})for the caller that is a program rather than a person. requestid.ServerOnlyGeneratorprevents clients from forging the IDs that reach your logs; the default generator trusts an inboundX-Request-Id.- The deeper treatments live in their own chapters: authentication in Chapter 10, caching in Chapter 14, i18n in Chapter 15, observability in Chapter 17.
Further Reading
- MDN: Cross-Origin Resource Sharing (CORS):
the same-origin policy, preflights and every
Access-Control-*header explained. - MDN: HTTP caching:
how
Cache-Control,ETagand conditional requests interact. - MDN: Using HTTP cookies:
cookie attributes (
Secure,SameSite, expiry) that session security relies on. - RFC 7519: JSON Web Token. The JWT specification: claims, structure and validation rules.
- OWASP Session Management Cheat Sheet: session ID rotation, expiry and cookie hardening in practice.
- net/http/pprof package documentation:
the Go profiling endpoints the
pprofmiddleware mounts.