<!--
  Configuration Management
  From the Iris book, chapter 11, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/configuration-management/
-->
# Chapter 11: Configuration Management

Iris keeps its configuration in one plain struct: `iris.Options`. You build
the value in code, load it from YAML, or read it from an environment variable,
then hand it to `iris.New` or `iris.NewBuilder`. Every field is optional
because the framework merges what you provide with its defaults. This chapter
walks through the struct, the merge rules that make partial configuration
safe, the loading helpers, the router and context option tours, and the
server options passed to `app.Run`.

## Background

Configuration is every value your program needs that is not part of its
logic: which port to listen on, how verbose the logs are, which database to
talk to, which password unlocks the admin account. The same compiled binary
typically runs in several environments (your laptop, a staging server, the
production cluster) and each needs different values. If those values live
in the source code, every difference forces a rebuild, and every credential
becomes visible to anyone who can read the code. The twelve-factor
methodology, a widely cited set of principles for deployable applications,
distills this into one rule: keep configuration in the environment, so the
code stays identical across deployments and only the surrounding values
change.

Two carriers for configuration appear throughout this chapter. A **YAML
file** is a plain-text format for structured data: `key: value` pairs,
nesting expressed by indentation, lists introduced with `-`. People can
read and edit it easily, which makes it the usual choice for settings that
live next to the code, such as development defaults. An **environment
variable** is a named string the operating system hands a process when it
starts. It is set outside the program (by the shell, a container runtime,
a systemd unit or an orchestration tool) and read at run time with a call
like `os.Getenv`. Because every deployment platform knows how to inject
environment variables, they are the natural channel for anything that
changes per deployment.

A **secret** is a configuration value that grants access: a password, an
API key, a signing key, a database connection string. Secrets never belong
in a repository, even a private one. Version control remembers everything,
so a key committed once stays in the history long after the file is
"fixed"; clones, forks, backups and CI machines all receive a copy; and
revoking a leaked key is far more painful than never leaking it. The safe
home for secrets is the environment, filled either by hand or by a secret
manager (a vault service that stores secrets encrypted and injects them
at deploy time).

Iris makes this discipline cheap to follow. One struct, `iris.Options`,
holds all framework settings; it merges with sensible defaults, loads from
YAML or a single environment variable, and embeds into your own config
struct. What disappears from your code is the configuration plumbing
itself: no flag-parsing forest, no per-environment build tags, no
hand-rolled merge of defaults against overrides.

## The Options Struct

```go
package main

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

func main() {
    app := iris.New(iris.Options{
        Name:     "bookstore",
        LogLevel: "debug",
        Host:     "example.com",
    })

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

    app.Listen(":8080")
}
```

`iris.Options` has five fields, all optional:

| Field      | Type                  | Purpose                                                                                                                       |
| ---------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `Name`     | `string`              | Application name, used as the log prefix of the application's logger.                                                         |
| `LogLevel` | `string`              | `"disable"`, `"fatal"`, `"error"`, `"warn"`, `"info"` (default) or `"debug"`.                                                 |
| `Host`     | `string`              | The trusted domain the server runs on, e.g. `"example.com"`. Also copied to `Router.Host`, which subdomain routing relies on. |
| `Router`   | `iris.RouterOptions`  | Routing behavior for all routes.                                                                                              |
| `Context`  | `iris.ContextOptions` | Request and response handling for all routes.                                                                                 |

`Options` implements the `iris.Configurator` interface, and `iris.New`
accepts any number of configurators. For imperative setup steps you can also
pass an `iris.ConfiguratorFunc(func(app *iris.Application) { ... })`, which
runs against the application instance at construction time.

## Merge Semantics

An `Options` literal passed to `iris.New` merges with `iris.DefaultOptions`:
every field left at its zero value is filled from the defaults. A partial
literal therefore configures only what it sets. The example above changes the
name and the log level; the charset stays `"utf-8"`, the multipart form limit
stays 32 MB, and so on.

Two refinements to that rule matter in practice.

**A non-nil empty slice or map is an explicit "none".** `nil` means "unset,
give me the default", while an allocated empty value means "I want nothing
here". The field where the distinction is visible out of the box is
`RemoteAddrPrivateSubnets`, whose default is a list of common private IP
ranges:

```go
import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/core/pkg/netutil"
)

app := iris.New(iris.Options{
    Context: iris.ContextOptions{
        // nil would inherit the default private ranges;
        // an empty non-nil slice clears them.
        RemoteAddrPrivateSubnets: []netutil.IPRange{},
    },
})
```

**A meaningful zero cannot be expressed at construction.** For scalar fields
the merge cannot tell "unset" apart from "deliberately zero". Setting
`URLParamSeparator: ""` in the literal does nothing, because the empty string
counts as unset and is replaced by the default `","`. The escape hatch is
per-request mutation: `ctx.Options()` returns a request-scoped copy of the
context options that a middleware may edit freely, and the change dies with
the request:

```go
app.Use(func(ctx iris.Context) {
    // Treat "a,b" as one value instead of splitting on commas,
    // for this request only.
    ctx.Options().URLParamSeparator = ""
    ctx.Next()
})
```

The mechanism is not limited to zeros. Any handler can adjust the context
options for the request it serves. A route group that accepts large uploads,
for example, can raise `BodyMaxMemory` in its middleware while the rest of
the application keeps the strict global cap.

## Defaults and EditOptions

`iris.DefaultOptions` is the exported default configuration. When you want
the defaults plus a few edits, `iris.Defaults` reads better than a literal
because you modify the defaults in place:

```go
app := iris.New(iris.Defaults(func(opts *iris.Options) {
    opts.LogLevel = "debug"
    opts.Context.RemoteAddrHeaders = []string{"X-Forwarded-For"}
}))
```

After construction, `app.EditOptions` applies further changes:

```go
app := iris.New()

app.EditOptions(func(opts *iris.Options) {
    opts.LogLevel = "warn"
    opts.Context.BodyMaxMemory = 8 * iris.MB
})
```

`EditOptions` must run before the application is built, that is before
`Build`, `Listen` or `Run`. Once built it only logs a warning and the change
may not be applied, so never call it from inside a request handler. For
per-request changes use `ctx.Options()` as shown above.

## YAML Files and the SERVER\_CONFIG Variable

Two helpers load `Options` from the outside world.

`iris.YAML` reads a YAML file, applies it over the defaults and panics when
the file is missing or malformed. It fits programs where the file is
required:

```go
app := iris.New(iris.YAML("config.yml"))
```

The YAML keys follow the struct's YAML tags, which are CamelCase and nest the
router and context sections:

```yaml
# config.yml
Name: bookstore
LogLevel: info
Host: example.com
Router:
    ForceLowercaseRouting: true
    FireMethodNotAllowed: true
Context:
    BodyMaxMemory: 4194304 # 4 MB
    RemoteAddrHeaders:
        - X-Forwarded-For
```

`(*Options).Bind` is environment-first: it reads the `SERVER_CONFIG`
environment variable as a full YAML document and only falls back to the given
file when the variable is empty. It returns an error instead of panicking:

```go
package main

import (
    "log"

    "github.com/kataras/iris/v14"
)

func main() {
    var opts iris.Options
    if err := opts.Bind("config.yml"); err != nil {
        log.Fatal(err)
    }

    app := iris.New(opts)
    app.Listen(":8080")
}
```

One container variable can carry the entire configuration, so a deployment
does not need to mount files. Fields the document leaves out still inherit
the defaults when the value reaches `iris.New`.

All four inputs meet at the same merge, and the two rules that catch people
out are consequences of it: an empty-but-non-nil collection survives, and a
meaningful zero does not.

\[Diagram: Whichever way you supply options, every field you left unset is filled from the defaults, Configure applies them, EditOptions may re-run the merge until Build freezes it, and ctx.Options() is the per-request escape hatch afterwards.]

## Router Options

`iris.RouterOptions` controls matching behavior for all routes. Every boolean
defaults to false.

| Field                              | Effect                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Host`                             | The server's host, including a non-standard port. Set automatically from `Options.Host`; subdomain routing depends on it.                                                                                                                                                                                                                                                                                       |
| `DisablePathCorrection`            | By default a request to `/home/` is redirected (301) to a registered `/home`. Set to true to disable.                                                                                                                                                                                                                                                                                                           |
| `DisablePathCorrectionRedirection` | With path correction on, execute the corrected route's handler directly instead of redirecting.                                                                                                                                                                                                                                                                                                                 |
| `EnablePathIntelligence`           | Redirect not-found GET requests to the closest registered path, e.g. `/cont` to `/contact`.                                                                                                                                                                                                                                                                                                                     |
| `EnablePathEscape`                 | Keep encoded characters such as `%2F` inside path parameters.                                                                                                                                                                                                                                                                                                                                                   |
| `ForceLowercaseRouting`            | Register and match all paths lowercased.                                                                                                                                                                                                                                                                                                                                                                        |
| `AllowQuerySemicolons`             | Convert unescaped `;` in the query to `&` (the pre-Go 1.17 behavior).                                                                                                                                                                                                                                                                                                                                           |
| `EnableDynamicHandler`             | Allow adding routes at serve time via `RefreshRouter`, at the cost of a routing mutex on every request.                                                                                                                                                                                                                                                                                                         |
| `FireMethodNotAllowed`             | Answer 405 instead of 404 when the path matches but the method does not. A builder application answers it with the `rest/errors` JSON envelope; for your own body, register `HandleNoMethod` from a `Configurator` passed to `NewBuilder`, because the builder fills that slot at `Build` ([Chapter 3](https://iris-go.com/docs/iris-newbuilder/), [Chapter 4](https://iris-go.com/docs/routing-controllers/)). |

```go
app := iris.New(iris.Options{
    Router: iris.RouterOptions{
        ForceLowercaseRouting: true,
        FireMethodNotAllowed:  true,
    },
})
```

## Context Options

`iris.ContextOptions` configures request and response handling. The same
struct is editable per request through `ctx.Options()`.

Request fields:

| Field                      | Default                           | Effect                                                                                                                                                                                                                                          |
| -------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `URLParamSeparator`        | `","`                             | Separator used to split multi-value entries in `ctx.Query()`, `ctx.Form()` and `ctx.PostForm()`.                                                                                                                                                |
| `FireEmptyFormError`       | `false`                           | Make `ReadForm`, `ReadQuery` and `ReadBody` return `iris.ErrEmptyForm` on empty form data instead of succeeding silently.                                                                                                                       |
| `TimeFormat`               | `"Mon, 02 Jan 2006 15:04:05 GMT"` | Datetime parsing format, used for example by `ctx.CheckIfModifiedSince`.                                                                                                                                                                        |
| `PostMaxMemory`            | 32 MB                             | Memory cap for multipart form parsing.                                                                                                                                                                                                          |
| `BodyMaxMemory`            | 0 (no limit)                      | Byte cap for `Body`, `ReadJSON`, `ReadJSONStream`, `ReadMultipartRelated` and the readers built on them, including `ctx.BindJSON`. Exceeding it fails with the `context` package's `ErrRequestEntityTooLarge`, which `rest/errors` maps to 413. |
| `RemoteAddrHeaders`        | none                              | Header names trusted to reveal the real client IP in `ctx.RemoteAddr()`. Empty by default: no `X-` header is trusted unless you list it.                                                                                                        |
| `RemoteAddrHeadersForce`   | `false`                           | Use the first header entry as the client IP even when it falls inside a private subnet.                                                                                                                                                         |
| `RemoteAddrPrivateSubnets` | common private ranges             | IP ranges treated as private when resolving the client IP.                                                                                                                                                                                      |
| `SSLProxyHeaders`          | none                              | Header name/value pairs that mark a request as HTTPS for `ctx.IsSSL()`, e.g. `{"X-Forwarded-Proto": "https"}`.                                                                                                                                  |
| `HostProxyHeaders`         | none                              | Headers that may carry the proxied hostname for `ctx.Host()`.                                                                                                                                                                                   |
| `RequestValidator`         | `nil`                             | Called after body decoding on `ReadJSON`, `ReadForm`, `ReadQuery` and friends.                                                                                                                                                                  |

Response fields:

| Field                 | Default           | Effect                                                                                                 |
| --------------------- | ----------------- | ------------------------------------------------------------------------------------------------------ |
| `Charset`             | `"utf-8"`         | Character set appended to rendered Content-Type headers.                                               |
| `ErrorHandler`        | framework default | Handles errors produced while writing responses through `JSON`, `XML`, `Render` and the other writers. |
| `DisableErrorHandler` | `false`           | Turn the write-error handler off entirely.                                                             |

In production, requests often reach your server through a **reverse
proxy**: an intermediary such as a load balancer, nginx or a CDN that
accepts the client's connection and forwards it to your application. To
your program every request then appears to come from the proxy's own IP,
often over plain HTTP, so proxies attach headers such as
`X-Forwarded-For` (the original client IP) and `X-Forwarded-Proto` (the
original scheme). These headers are ordinary text that any client could
forge, which is why Iris trusts none of them until you explicitly list the
ones your own proxy sets.

A typical proxy-aware production configuration:

```go
app := iris.New(iris.Options{
    Context: iris.ContextOptions{
        BodyMaxMemory:     8 * iris.MB,
        RemoteAddrHeaders: []string{"X-Forwarded-For", "X-Real-Ip"},
        SSLProxyHeaders: map[string]string{
            "X-Forwarded-Proto": "https",
        },
    },
})
```

`BodyMaxMemory` is the application-wide cap; for per-route limits use the
`bodylimit` middleware instead ([Chapter 12](https://iris-go.com/docs/built-in-middleware/)).

`RequestValidator` accepts any value with a `Struct(any) error` method. The
third-party `github.com/go-playground/validator/v10` package satisfies it
directly:

```go
import "github.com/go-playground/validator/v10"

app.EditOptions(func(opts *iris.Options) {
    opts.Context.RequestValidator = validator.New()
})
```

See [Chapter 5](https://iris-go.com/docs/reading-requests/) for the body readers themselves.

## Embedding Options in Your Own Config

Real applications have settings of their own next to the framework's.
Embedding `iris.Options` inline in the application's config struct makes one
YAML document configure both, and the loader below is that shape written out
in full, because it is worth seeing once. It is not what the tutorial or the
rest of this book does any more: they embed `iris.BaseConfiguration`, which
carries `Host`, `Port`, `AllowOrigin`, `EnableCompression`, `RequestLog` and
the framework options under an `Iris` key, and they load it with the function
the framework ships. [The next section](#the-loader-the-framework-ships) is
the version to copy into a new service.

```go
package main

import (
    "fmt"
    "log"
    "os"

    "gopkg.in/yaml.v3"

    "github.com/kataras/iris/v14"
)

// Config holds the whole application configuration.
// AdminPassword is yaml:"-" on purpose: secrets come from the
// environment, never from a committed file.
type Config struct {
    iris.Options `yaml:",inline"`

    Addr          string `yaml:"Addr"`
    Env           string `yaml:"Env"`
    AdminUser     string `yaml:"AdminUser"`
    AdminPassword string `yaml:"-"`
}

// LoadConfig starts from working defaults, overlays YAML from the
// APP_CONFIG environment variable or the config.yml file (both
// optional), then reads the secrets from the environment.
func LoadConfig() (Config, error) {
    cfg := Config{
        Options:   iris.DefaultOptions,
        Addr:      ":8080",
        Env:       "development",
        AdminUser: "admin",
    }

    if raw := os.Getenv("APP_CONFIG"); raw != "" {
        if err := yaml.Unmarshal([]byte(raw), &cfg); err != nil {
            return cfg, fmt.Errorf("config from env: %w", err)
        }
    } else if data, err := os.ReadFile("config.yml"); err == nil {
        if err = yaml.Unmarshal(data, &cfg); err != nil {
            return cfg, fmt.Errorf("config.yml: %w", err)
        }
    }

    cfg.AdminPassword = os.Getenv("ADMIN_PASSWORD")
    if cfg.AdminPassword == "" {
        return cfg, fmt.Errorf("ADMIN_PASSWORD is required")
    }
    return cfg, nil
}

func main() {
    cfg, err := LoadConfig()
    if err != nil {
        log.Fatal(err)
    }

    app := iris.New(cfg.Options)
    // With the builder instead: iris.NewBuilder(cfg.Options)...
    // (see Chapter 3).

    app.Listen(cfg.Addr)
}
```

The committed `config.yml` contains settings only, no secrets:

```yaml
# Committed settings only. The admin password comes from
# the ADMIN_PASSWORD environment variable.
Name: bookstore
LogLevel: info
Addr: ":8080"
Env: development
AdminUser: admin
```

The `yaml:",inline"` tag makes the framework keys (`Name`, `LogLevel`,
`Host`, `Router`, `Context`) appear at the top level of the same document as
your own keys. The loading order mirrors `Options.Bind`: environment variable
first, file as the development fallback. Never commit credentials, key
material or connection strings, even in private repositories; the
environment, or a secret manager feeding it, is their only home.

### The loader the framework ships

That `LoadConfig` function is the same forty-five lines in every service
that has one, so the framework has its own:

```go
type Configuration struct {
    iris.BaseConfiguration `yaml:",inline"`

    DatabaseURL string `yaml:"DatabaseURL"`
}

func main() {
    c, err := iris.LoadConfiguration[Configuration](
        "MY_SERVICE_CONFIG", "configuration.yml")
    if err != nil {
        log.Fatal(err)
    }

    app := iris.New(c.Iris)
    // ...routes...
    app.Listen(c.Addr())
}
```

`iris.LoadConfiguration[T]` reads a YAML document into `T` from the named
environment variable when it is set, otherwise from the file. The variable
may hold either the document *or* a path to it: a value naming an existing
file is read as a file and anything else is decoded as YAML, so the same
binary works whether the deployment mounts the configuration or passes it
inline. It is not a generic `Options.Bind`; that one is hardcoded to
`SERVER_CONFIG` and always reads its value as a document.

If `T` (or `*T`) has a `BindEnv()` method it runs *after* decoding, which
is what makes the environment win over the document. Embedding
`iris.BaseConfiguration` gets that for the fields every service shares:

| Field               | YAML key            | Meaning                                                                                                                                     |
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `Host`              | `Host`              | The interface to listen on; empty means every interface. Not `Iris.Host`, which is the trusted domain the router matches subdomains against |
| `Port`              | `Port`              | The TCP port; zero means 8080                                                                                                               |
| `AllowOrigin`       | `AllowOrigin`       | The origin line, as `Builder.AllowOrigin` accepts it                                                                                        |
| `EnableCompression` | `EnableCompression` | As `Builder.Compression` accepts it                                                                                                         |
| `RequestLog`        | `RequestLog`        | As `Builder.LogRequests` accepts it                                                                                                         |
| `Iris`              | `Iris`              | The framework `Options`, as `iris.New` accepts them                                                                                         |

Its `BindEnv` reads `HOST`, `PORT`, `HTTP_PLATFORM_PORT` and
`ALLOW_ORIGIN`. `HTTP_PLATFORM_PORT` wins over `PORT`, because a hosting
platform that picks the port passes it there while `PORT` may still carry
the value baked into the image. A non-numeric port is ignored rather than
turned into zero, so a typo cannot silently move the server to 8080.
`Addr()` returns the `host:port` string for `app.Listen`, joined so that
an IPv6 host is bracketed correctly.

There is deliberately no `app.Serve(host, port)`. `Listen` and `Run` stay
the only two entry points, and `Addr()` is the part that was missing.

## Multi-Environment Setups

Because the whole configuration travels as one YAML document, environments do
not need file hierarchies or override matrices. Keep the development file in
the repository, and let each deployment carry its own document in the
environment variable:

```yaml
# deploy/production.yml, injected as APP_CONFIG
Name: bookstore
LogLevel: warn
Host: api.example.com
Addr: ":8080"
Env: production
Context:
    RemoteAddrHeaders:
        - X-Forwarded-For
    SSLProxyHeaders:
        X-Forwarded-Proto: https
```

```sh
# The deployment pipeline sets the variable; locally you can do:
APP_CONFIG="$(cat deploy/production.yml)" ./server
```

A Kubernetes ConfigMap, a systemd unit or a compose file sets the same
variable the same way. Unset fields still merge with the defaults, so each
document stays as small as the differences between environments.

## Server Options

Serving is configured separately from the application, at the call to
`app.Run`. `app.Listen(addr)` is the shorthand; the two are equivalent:

```go
app.Listen(":8080")

// is the same as:
app.Run(iris.ServerOptions{
    Addr:           ":8080",
    Network:        "tcp",
    KeepAlive:      3 * time.Minute,
    SocketSharding: true,
})
```

The fields of `iris.ServerOptions`:

| Field                     | Purpose                                                                                                                                                                                                                                                 |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Addr`                    | Listen address in `"host:port"` form. Empty means `":http"`, or `":https"` when `TLSConfig` is set.                                                                                                                                                     |
| `Network`                 | `"tcp"` (default), `"tcp4"`, `"tcp6"`, `"unix"` or `"unixpacket"`.                                                                                                                                                                                      |
| `KeepAlive`               | Keep-alive period for accepted connections; negative disables keep-alives.                                                                                                                                                                              |
| `SocketSharding`          | `SO_REUSEPORT` (`SO_REUSEADDR` on Windows) for multi-CPU scaling.                                                                                                                                                                                       |
| `Hosts`                   | Hostnames the TLS certificate is valid for; falls back to the application's `Host`, then to `Addr`.                                                                                                                                                     |
| `TLSConfig`               | Custom `*tls.Config`.                                                                                                                                                                                                                                   |
| `CertFile`, `KeyFile`     | Paths to PEM-encoded certificate and key files.                                                                                                                                                                                                         |
| `Letsencrypt`             | Automatic HTTPS via Let's Encrypt.                                                                                                                                                                                                                      |
| `Ngrok`, `SSHTunnel`      | Expose the local server through an ngrok or SSH reverse tunnel ([Chapter 19](https://iris-go.com/docs/deployment-devops/)). `Ngrok` needs the blank import `_ "github.com/kataras/iris/v14/core/server/ngrok"`; without it the server refuses to start. |
| `Timeout`                 | `*iris.TimeoutOptions`, see below.                                                                                                                                                                                                                      |
| `LogWriter`               | Destination for errors surfaced by the underlying `http.Server`.                                                                                                                                                                                        |
| `DisableStartupLog`       | Suppress the startup message.                                                                                                                                                                                                                           |
| `DisableInterruptHandler` | Skip the built-in interrupt handler that shuts the server down gracefully on CTRL+C.                                                                                                                                                                    |

**TLS** (Transport Layer Security) is the encryption layer that turns HTTP
into HTTPS. A server proves its identity with a **certificate** (a file,
signed by a trusted certificate authority, that binds a hostname to a
public key) and keeps the matching private key to itself. Let's Encrypt
is a certificate authority that issues certificates free of charge through
an automated protocol; the `Letsencrypt` option drives that protocol for
you, so the server obtains and renews its own certificates instead of you
installing files.

Use one TLS mechanism at a time: `TLSConfig`, the `CertFile`/`KeyFile` pair,
`Letsencrypt`, `Ngrok` or `SSHTunnel`. The one sanctioned combination is
`Letsencrypt` with `CertFile`/`KeyFile`, where the files act as a local
fallback when the Let's Encrypt challenge fails.

Serving TLS from certificate files:

```go
app.Run(iris.ServerOptions{
    Addr:     ":443",
    CertFile: "server.crt",
    KeyFile:  "server.key",
})
```

Automatic HTTPS:

```go
app.Run(iris.ServerOptions{
    Addr:        ":443",
    Hosts:       []string{"api.example.com"},
    Letsencrypt: true,
})
```

### Timeouts

A timeout is the maximum time you allow a network operation before giving
up on it. Servers need timeouts because clients can be slow or hostile on
purpose: a connection trickling one byte per second would otherwise hold
memory and a file descriptor forever. Keep-alive, mentioned below, is the
practice of reusing one TCP connection for several requests instead of
opening a fresh connection each time; the idle timeout bounds how long
such a reused connection may sit waiting for its next request.

`iris.TimeoutOptions` holds the standard `net/http` server timeouts plus an
optional handler timeout:

| Field                        | Effect                                                                           |
| ---------------------------- | -------------------------------------------------------------------------------- |
| `ShutdownOnInterruptTimeout` | How long graceful shutdown may take on interrupt. Defaults to 15 seconds.        |
| `ServerTimeout`              | One knob from which the read-header, write and idle timeouts are derived.        |
| `ReadTimeout`                | Maximum duration to read the whole request, body included.                       |
| `ReadHeaderTimeout`          | Time allowed to read request headers; handlers keep control over body deadlines. |
| `WriteTimeout`               | Maximum duration for writing the response.                                       |
| `IdleTimeout`                | Keep-alive wait between requests.                                                |
| `HandlerTimeout`             | Wraps the router in `http.TimeoutHandler` when greater than zero.                |
| `HandlerTimeoutMessage`      | HTML body sent when the handler timeout fires.                                   |

Start from the `iris.Timeout()` builder so the shutdown timeout and the
default timeout message stay in place, then set what you need:

```go
package main

import (
    "time"

    "github.com/kataras/iris/v14"
)

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

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

    app.Run(iris.ServerOptions{
        Addr: ":8080",
        Timeout: iris.Timeout().
            WithServerTimeout(30 * time.Second).
            WithHandlerTimeout(15 * time.Second),
    })
}
```

Unless `DisableInterruptHandler` is set, the server catches the interrupt
signal, stops accepting connections and waits up to
`ShutdownOnInterruptTimeout` for in-flight requests to finish.

## Summary

- `iris.Options` (`Name`, `LogLevel`, `Host`, `Router`, `Context`) is the
  whole application configuration; every field is optional.
- Options merge with `iris.DefaultOptions`: partial literals are safe, a
  non-nil empty slice or map means "explicitly none", and meaningful zeros
  are set per request through `ctx.Options()` in a middleware.
- `iris.Defaults(edits...)` tweaks the defaults in place; `app.EditOptions`
  applies changes before the application is built, never from a handler.
- `iris.YAML("config.yml")` loads a required file; `(*Options).Bind` reads
  the `SERVER_CONFIG` environment variable first and falls back to a file.
- Embed `iris.Options` into your own config struct with `yaml:",inline"` for
  one document that configures both the framework and your application; keep
  secrets in the environment only.
- Serving is configured at `app.Run(iris.ServerOptions{...})`: address, TLS
  (files, custom config or Let's Encrypt), keep-alive, socket sharding and
  timeouts via `iris.Timeout()`.

## Further Reading

- [The Twelve-Factor App: Config](https://12factor.net/config): the
  canonical statement of why configuration belongs in the environment,
  separate from code.
- [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html):
  practical guidance on storing, injecting and rotating secrets.
- [YAML 1.2 specification](https://yaml.org/spec/1.2.2/): the definitive
  reference for the format read by `iris.YAML` and `Options.Bind`.
- [yaml.v3 package documentation](https://pkg.go.dev/gopkg.in/yaml.v3):
  the Go YAML library used in this chapter's embedding example.
- [MDN: X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For):
  how proxy forwarding headers work and why they must not be trusted
  blindly.
- [Let's Encrypt documentation](https://letsencrypt.org/docs/): how
  automatic HTTPS certificate issuance and renewal work.
