<!--
  Built-in Middleware
  From the Iris book, chapter 12, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/built-in-middleware/
-->
# Chapter 12: Built-in Middleware

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](https://iris-go.com/docs/authentication-authorization/), caching strategies in
[Chapter 14](https://iris-go.com/docs/caching-strategies/), i18n in
[Chapter 15](https://iris-go.com/docs/internationalization-i18n/) and observability in
[Chapter 17](https://iris-go.com/docs/monitoring-observability/).

## Background

Middleware, introduced in [Chapter 2](https://iris-go.com/docs/core-concepts/), 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](#adapting-nethttp-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:

```go
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](https://iris-go.com/docs/iris-newbuilder/)), 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 by
  `recovery.Handler`, so panics become logged 500 INTERNAL responses. The
  builder skips it when a handler named `iris.recovery` or
  `iris.errors.recover` is already on the router.
- **CORS**: the `AllowOrigin(originLine)` step registers
  `cors.New().HandleErrorFunc(errors.FailedPrecondition.Err).
  AllowOrigin(originLine).Handler()`. The `Cors(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 set `HandleErrorFunc`
  to keep the structured error body.
- **Compression**: `Compression(true)` only records the choice; the
  `compress.Handler` itself is registered later, by the first
  `RouterMiddlewares` or `Middlewares` call, and on the root rather than
  the prefix router.
- **Access logging**: `LogRequests(true)` registers a tuned
  `accesslog.AccessLog` (JSON lines to `access_log.json` plus stdout) and
  closes it on shutdown; `LogRequestsWith(ac)` registers your own
  instance instead.
- **Health endpoint**: `Health(true, env, developer)` registers
  `modrevision.New` at `GET /health` on the root, outside the prefix.
- **Rate exceed handler**: the builder binds the `rate` middleware's
  429 response to the JSON error format of the `rest/errors` package.

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:

\[Diagram: The builder's middleware lands in one ordered pre-routing chain: CORS and the access log first because their steps register them directly, then the four that the first RouterMiddlewares call installs, and the mux last.]

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.

```go
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:

```go
// 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.

```go
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:

```go
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.

```go
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`:

```go
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](https://iris-go.com/docs/error-handling/).

## 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:

```go
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.

```go
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:

```go
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".

```go
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](https://iris-go.com/docs/reading-requests/).

## 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.

```go
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](https://iris-go.com/docs/caching-strategies/).

## 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`.

```go
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](https://iris-go.com/docs/authentication-authorization/).

## 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.

```go
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](https://iris-go.com/docs/authentication-authorization/).

## 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:

```go
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.

```go
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](https://iris-go.com/docs/authentication-authorization/) 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.

```go
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:

```go
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.

```go
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](https://iris-go.com/docs/internationalization-i18n/).

## Views

The `view` middleware connects any template engine that implements a
single-method interface:

```go
type Engine interface {
    ExecuteTemplate(w io.Writer, templateName string,
        layoutName string, data any) error
}
```

Register the engine once, render from handlers:

```go
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](https://iris-go.com/docs/response-handling/).

## 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.

```go
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](https://iris-go.com/docs/monitoring-observability/).

**monitor** renders live process and OS statistics (CPU, RAM,
connections, load):

```go
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) // JSON
```

**httpcost** measures what each request costs in elapsed time, memory
and CPU, using configurable per-unit prices:

```go
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:

```go
import "github.com/kataras/iris/v14/middleware/pprof"

app.Router("/debug/pprof", pprof.New())
// then: go tool pprof http://localhost:8080/debug/pprof/profile
```

Guard 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:

```go
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:

```go
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:

```go
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:

```go
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:

```go
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`:

```go
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:

```go
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:

```go
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:

```go
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.

```go
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:

```go
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:

```go
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:

```go
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`:

```go
// 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`:

```go
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](https://github.com/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:

```sh
$ go get github.com/iris-contrib/middleware/casbin@master
```

Sentry maintains its own Iris handler in
[getsentry/sentry-go](https://github.com/getsentry/sentry-go/tree/master/iris)
rather than here.

Two entries in that repository, `cors` and `jwt`, predate the framework's
own. Use the built-in
[`middleware/cors`](#cors) and [`middleware/jwt`](#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: `UseRouter` for infrastructure that must
  see every request (CORS, compression, access logs, rewrites,
  recovery), `Use` for request-scoped behavior (rate and body limits,
  sessions, authentication, cache headers).
- `iris.NewBuilder` already installs recovery, and (through its chain
  steps) CORS, compression, access logging and the `/health` endpoint,
  all wired to the `rest/errors` JSON 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 its `CacheControlBuilder`,
  typed `sessions` with secure cookie defaults, a `jwt` verifier
  that reads the `Authorization` header by default, and
  `apikey.New(apikey.Options{Keys: ...})` for the caller that is a
  program rather than a person.
- `requestid.ServerOnlyGenerator` prevents clients from forging the IDs
  that reach your logs; the default generator trusts an inbound
  `X-Request-Id`.
- The deeper treatments live in their own chapters: authentication in
  [Chapter 10](https://iris-go.com/docs/authentication-authorization/), caching in
  [Chapter 14](https://iris-go.com/docs/caching-strategies/), i18n in
  [Chapter 15](https://iris-go.com/docs/internationalization-i18n/), observability in
  [Chapter 17](https://iris-go.com/docs/monitoring-observability/).

## Further Reading

- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS):
  the same-origin policy, preflights and every `Access-Control-*` header
  explained.
- [MDN: HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching):
  how `Cache-Control`, `ETag` and conditional requests interact.
- [MDN: Using HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies):
  cookie attributes (`Secure`, `SameSite`, expiry) that session security
  relies on.
- [RFC 7519: JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519).
  The JWT specification: claims, structure and validation rules.
- [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html):
  session ID rotation, expiry and cookie hardening in practice.
- [net/http/pprof package documentation](https://pkg.go.dev/net/http/pprof):
  the Go profiling endpoints the `pprof` middleware mounts.
