<!--
  Reading Requests
  From the Iris book, chapter 5, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/reading-requests/
-->
# Chapter 5: Reading Requests

Every handler receives an `iris.Context`, and half of its surface exists to
answer one question: what did the client send? This chapter covers that
half: request metadata, typed path and query accessors, body decoding with
the recommended `ctx.BindJSON` path and the lower-level `ctx.Read*` family,
pagination input, forms and file uploads, cookies, body size limits, and the
application-level validation hook. Writing responses is the subject of
[Chapter 6](https://iris-go.com/docs/response-handling/).

## Background

An HTTP request is a small, structured text message. Its first line names
a method and a path: the method says what kind of action the client
intends (`GET` reads, `POST` creates, `PUT` replaces, `DELETE` removes),
and the path says which resource the action targets. Headers come next,
then optionally a body. Data can arrive in four distinct places, and each
place has a job:

- **The path** identifies the resource. In `/users/42`, the `42` is a
  path parameter (part of the URL itself), so it is always present and
  always text until you convert it.
- **The query string** is everything after the `?`. In
  `/search?q=iris&page=2`, `q` and `page` are query parameters: they
  refine a request (filters, sorting, page numbers) without changing
  which resource it targets.
- **Headers** are key-value metadata about the request: who the client
  is (`User-Agent`), what credentials it carries (`Authorization`), what
  format the body is in (`Content-Type`).
- **The body** is the payload itself: the JSON document to store, the
  form fields, the uploaded file. Only some methods carry one; a `GET`
  usually does not.

The `Content-Type` header tells the server how to interpret the body's
bytes, using a media type (also called a MIME type) such as
`application/json` or `text/html`. JSON is the dominant one for APIs: a
plain-text format defined by RFC 8259, with exactly six shapes: objects
(`{}`), arrays (`[]`), strings, numbers, booleans and `null`.
Because it is plain text, every language can produce and read it, which
is why it became the common tongue between web services.

HTML forms have their own two media types. A simple form posts
`application/x-www-form-urlencoded`: field names and values joined into
one `name=value&...` string, essentially a query string moved into the
body. A form with a file input posts `multipart/form-data`: the body is
split into parts separated by a boundary marker, each part carrying its
own headers, so text fields and raw file bytes travel in one request.

One rule shapes everything in this chapter: never trust the client.
Every piece of a request (path, query, headers, body, cookies, even the
declared `Content-Type`) is chosen by the sender, and the sender is not
always a well-behaved browser. Anyone can send any bytes with a one-line
`curl` command, so client-side checks (a form's `required` attribute, a
JavaScript validator) are conveniences, not guarantees. Server-side
validation is the real gate, and it is what this chapter's reading
helpers build in: `ctx.BindJSON` decodes *and* validates before your
logic runs, the typed accessors refuse malformed numbers, and body
limits stop a client from streaming gigabytes at your decoder. The
problem that disappears from your code is manual string parsing and
defensive `if` ladders: handlers receive typed, validated values or
never run at all.

## Request Metadata

The basics (method, path, host) are one call away:

```go
package main

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

func main() {
    app := iris.New()
    app.Get("/info", info)
    app.Listen(":8080")
}

func info(ctx iris.Context) {
    ctx.JSON(iris.Map{
        "method":    ctx.Method(),         // "GET"
        "path":      ctx.Path(),           // "/info"
        "uri":       ctx.FullRequestURI(), // "http://localhost:8080/info"
        "scheme":    ctx.Scheme(),         // "http://"
        "host":      ctx.Host(),           // "localhost:8080"
        "hostname":  ctx.Hostname(),       // "localhost"
        "subdomain": ctx.Subdomain(),      // "" unless e.g. api.example.com
        "ip":        ctx.RemoteAddr(),
    })
}
```

When you need the raw `*http.Request`, `ctx.Request()` returns it.

### The client address

`ctx.RemoteAddr()` returns the client IP. By default it parses only the
transport-level `Request.RemoteAddr`: proxy headers such as
`X-Forwarded-For` are ignored, because any client can forge them. Enable
them only when your server sits behind a proxy you control:

```go
app := iris.New(iris.Options{
    Context: iris.ContextOptions{
        RemoteAddrHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
    },
})
```

With `RemoteAddrHeaders` set, `RemoteAddr` scans the listed headers in
order and returns the first public IP; candidates inside the private
ranges listed in `RemoteAddrPrivateSubnets` are skipped unless you also
set `RemoteAddrHeadersForce`. The related `ctx.Addr()` returns the first
candidate even when it belongs to a private network. Never base security
decisions solely on a client-reported address.

### Request headers

`ctx.Header(name)` reads a **request** header. The response counterpart
is `ctx.ResponseHeader(name)`, covered in
[Chapter 6](https://iris-go.com/docs/response-handling/):

```go
auth := ctx.Header("Authorization")
agent := ctx.Header("User-Agent")
```

One deliberate quirk: when the client repeats a header, `ctx.Header`
returns all values joined by `", "` instead of only the first (which is
what `http.Header.Get` would do). Code that compares the result against a
secret therefore rejects a request carrying two copies of the header
rather than silently honoring the first one.

Headers also bind to a struct through the `header` field tag:

```go
type ClientInfo struct {
    UserAgent string `header:"User-Agent"`
    RequestID string `header:"X-Request-ID"`
}

var info ClientInfo
if err := ctx.ReadHeader(&info); err != nil {
    errors.InvalidArgument.Err(ctx, err)
    return
}
```

### Content type of the request

`ctx.ContentType()` parses the request's `Content-Type` header into a
structured value; `ctx.MediaType()` returns just the media type string,
and `ctx.Charset()` and `ctx.ContentLength()` complete the picture:

```go
ct := ctx.ContentType()   // ContentType{MediaType, Charset, Boundary, ...}
mt := ctx.MediaType()     // e.g. "application/json"
cs := ctx.Charset()       // e.g. "utf-8", "" when absent
n := ctx.ContentLength()  // int64, 0 when the header is missing
```

These are request-side methods. The response equivalents are
`ctx.ResponseContentType()` and `ctx.ResponseMediaType()`.

## Path and Query Parameters

### Path parameters

Dynamic route segments (`{id:int64}`, `{slug}`; see
[Chapter 4](https://iris-go.com/docs/routing-controllers/) for the macro types) are read
through `ctx.Params()`. Accessors return the value directly with a
type-specific default on a missing key or failed conversion; there are
no `(value, error)` tuples:

```go
app.Get("/users/{id:int64}/posts/{slug}", func(ctx iris.Context) {
    id := ctx.Params().GetInt64("id")      // int64, -1 on failure
    slug := ctx.Params().GetString("slug") // string, "" on failure
    // ...
})
```

| Accessor               | Failure default                          |
| ---------------------- | ---------------------------------------- |
| `GetString`            | `""`                                     |
| `GetInt`, `GetInt64`   | `-1`                                     |
| `GetUint`, `GetUint64` | `0`                                      |
| `GetFloat64`           | `-1`                                     |
| `GetBool`              | `false`                                  |
| `GetUUID`              | zero `uuid.UUID` (equal to `uuid.Nil()`) |
| `GetDuration`          | `0`                                      |
| `GetTime`              | zero `time.Time`                         |
| `GetIP`                | zero `netip.Addr` (`IsValid()` false)    |

When the default is ambiguous (is `-1` a real value or a miss?), use
`Lookup`, which reports presence, or `Exists`:

```go
if raw, ok := ctx.Params().Lookup("id"); ok {
    _ = raw // the stored value, typed by the route macro.
}
```

A request whose typed parameter does not match the macro (say
`/users/abc` against `{id:int64}`, or a malformed `{id:uuid}`) never
reaches your handler; applications built with `iris.NewBuilder` render a
400 `INVALID_ARGUMENT` ("invalid path parameter") response automatically.

The context has a generic method that writes the error response for you
(404 when the parameter is missing, 400 when it is not of type `T`), so
the handler only handles the happy path:

```go
app.Get("/bookmarks/{id:int64}", func(ctx iris.Context) {
    id, ok := ctx.BindParam[int64]("id")
    if !ok {
        return // error response already sent.
    }
    // ...
})
```

To bind all path parameters at once, `ctx.ReadParams(&dst)` decodes into
a struct using the `param` field tag; `ctx.BindParams[S]()` does the
same and writes the error response itself, returning `(S, bool)`.

`T` is the type the macro stored, not the type you would like: `{id:int64}`
stores an `int64`, and `{id:uuid}` stores a parsed `uuid.UUID`, so a uuid
parameter is read as `ctx.BindParam[uuid.UUID]("id")` or
`ctx.Params().GetUUID("id")`. Asking for `string` there answers 400,
because the assertion fails. `ctx.Params().GetString("id")` still works
and returns the canonical lowercase text, whatever spelling the client sent.

### Query parameters

`ctx.Query()` returns a read-only, sorted view of the URL query with the
same typed accessors and the same failure defaults as `Params()`:

```go
// GET /search?q=iris&page=2&active=true&tags=go&tags=web
q := ctx.Query().GetString("q")        // "iris"
page := ctx.Query().GetInt("page")     // 2, -1 if missing or invalid
active := ctx.Query().GetBool("active") // true, false if missing
price := ctx.Query().GetFloat64("max") // -1 if missing

exists := ctx.Query().Exists("q")      // true
val, ok := ctx.Query().Lookup("q")     // "iris", true
```

A few getters parse the text into a richer type and return its zero
value on a miss. `GetDuration` reads `?ttl=1h30m` as 90 minutes,
`GetTime` reads an RFC 3339 timestamp such as `?since=2026-10-10T09:00:00Z`,
and `GetIP` turns `?addr=10.0.0.7` into a `netip.Addr` whose `IsValid()`
is false when the value is not an address:

```go
ttl := ctx.Query().GetDuration("ttl")    // time.Duration, 0 if missing or invalid
since := ctx.Query().GetTime("since")    // time.Time, zero if missing or invalid
addr := ctx.Query().GetIP("addr")        // netip.Addr
ref := ctx.Query().GetUUID("ref")        // uuid.UUID, uuid.Nil() if missing or invalid
```

Repeated keys are available in two forms:

```go
tags := ctx.Query().GetSlice("tags")        // ["go", "web"]: the original
                                            // []string from the query map.
cols := ctx.Query().GetStringSlice("columns") // splits the joined value by
                                              // the configured separator.
```

`GetStringSlice` splits on `ContextOptions.URLParamSeparator` (default
`","`), so `?columns=a,b,c` and `?columns=a&columns=b&columns=c` both
produce three elements; `GetSlice` returns the raw multi-value list as
the client sent it. There is no `URLParam` method family in v14. Use
the `Query()` accessors or `ReadQuery`.

Struct binding uses the `url` tag, and `ReadURL` combines path parameters
with the query in one call:

```go
type SearchFilter struct {
    UserID   int      `param:"id"`   // from the path.
    Keyword  string   `url:"q"`      // from the query.
    Tags     []string `url:"tags"`
    Page     int      `url:"page"`
}

app.Get("/users/{id:int}/search", func(ctx iris.Context) {
    var filter SearchFilter
    if err := ctx.ReadURL(&filter); err != nil {
        errors.InvalidArgument.Err(ctx, err)
        return
    }
    ctx.JSON(filter)
})
```

`ctx.ReadQuery(&dst)` does the same for the query alone.

### Strict query readers

The accessors above are lenient by design: `GetInt("page")` answers `-1`
for a missing key *and* for `?page=banana`, and a filter built on them
silently returns the wrong rows instead of telling the client it sent
nonsense. Three readers make that a 400 instead:

```go
// GET /reports?from=2026-01-01&limit=50&detailed=yes
from, ok := ctx.QueryDate("from", iris.QueryRequired())
if !ok {
    return // 400 already written: missing, or not a date.
}

limit, ok := ctx.QueryInt("limit", iris.QueryDefault("25"))
if !ok {
    return
}

detailed, ok := ctx.QueryBool("detailed")
if !ok {
    return
}
```

The second result is the `ok` of the `Bind*` family: `false` means the
response was already written and the handler returns. A present but
unparsable value is always a 400 naming the key and the shape expected,
never a silent zero. An absent value is the zero value unless an option
says otherwise.

| Option                       | Effect                                                                                                                                                |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iris.QueryRequired()`       | An absent or blank value is a 400 naming the key                                                                                                      |
| `iris.QueryDefault(raw)`     | The raw string to parse when the key is absent, stated in the client's own wire form (`"2020-01-02"`, `"25"`, `"true"`). It wins over `QueryRequired` |
| `iris.QueryZeroValues(v...)` | The placeholders older clients send for "unset" (`"0"`, `"zero"`, `"0000-00-00"`, `"null"`), each read exactly like an absent value                   |

`QueryDate` parses `types.SimpleDate` (`2006-01-02`, or `2006-1-2`) and
`QueryBool` accepts what `strconv.ParseBool` accepts plus `yes`, `no`,
`on` and `off`, case-insensitively. Nothing is special-cased below the
options: `?from=null` is a 400 unless `QueryZeroValues("null")` declared
it, because what a raw value means is the endpoint's decision and not the
parser's. An unparsable `QueryDefault` is a programmer error and panics on
the first request rather than rendering as the client's fault.

An endpoint that answers to more than one spelling of the same parameter
states them once, separated by a space, and each is tried in the order
given until one carries a value:

```go
date, ok := ctx.QueryDate("date day from") // ?day=2026-01-02 answers here
if !ok {
    return
}
```

A URL query key cannot contain a space, so a key without one is a single
key and reads exactly as it always did. `QueryInt` and `QueryBool` take
the same. When no key carried a value the 400 names all of them; when one
carried an unparsable value it names only that one, since the client did
not send the others.

## Binding JSON Requests

For JSON APIs the recommended entry point is the generic context method
`ctx.BindJSON[T]()`. It decodes the body, runs the payload's own
`Validate() error` method when the type implements it (the
`iris.Validatable` interface), and on any failure writes a structured
4xx response and returns `false`. The handler simply returns. You never
call `Validate` by hand.

The `rest/errors/validation` package provides small field builders for
those `Validate` methods:

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors/validation"
)

type CreateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

// Implementing Validate makes ctx.BindJSON run it after decoding.
func (in CreateUserInput) Validate() error {
    return validation.Join(
        validation.String("name", in.Name).NotEmpty().Length(2, 100),
        validation.String("email", in.Email).Email(),
        validation.Number("age", in.Age).Positive(),
    )
}

func createUser(ctx iris.Context) {
    input, ok := ctx.BindJSON[CreateUserInput]()
    if !ok {
        return // 400 already written: malformed JSON or failed Validate.
    }

    // input is decoded AND valid here.
    ctx.Created(input.Name, nil) // see Chapter 6 for responses.
}

func main() {
    app := iris.New()
    app.Post("/users", createUser)
    app.Listen(":8080")
}
```

A failed `Validate` renders each field error individually:

```json
{
    "http_error_code": {"canonical_name": "INVALID_ARGUMENT", "status": 400},
    "message": "validation failure",
    "detail": "fields were invalid",
    "validation": [
        {"field": "name", "value": "",
         "reason": "must not be empty, must be between 2 and 100 characters"},
        {"field": "age", "value": 0, "reason": "must be positive"}
    ]
}
```

The builders cover strings (`NotEmpty`, `Length`, `Email`, `Fullname`),
numbers (`Positive`, `Negative`, `NonZero`, `InRange`) and slices, and
compose with plain functions through `Func` for custom rules.
`validation.Join` drops entries that passed, and returns `nil` when
everything did.

### Optional bodies and variants

`ctx.BindJSONOptional[T]()` behaves like `BindJSON` except that an empty
request body is a success: it returns the zero value of `T` with `true`
and skips validation. Use it when the body is a nice-to-have, such as an
optional filter object.

Two more variants accept a body that may be either a single object or an
array of objects: `ctx.BindJSONOneOrMany[T]()` and
`ctx.BindJSONList[T]()` (which normalizes a single object
into a one-element slice). Every element is validated.

### Partial updates and the column list

A `PATCH` sends the body and the list of fields it means to change. The
list travels in the `columns` query key, and reading it apart from the
body invites the bug the whole family exists to prevent: a typo matches no
column, the update changes nothing, and the client is told it succeeded.
`ctx.BindUpdate[T]()` reads both and validates them against each other:

```go
func updateUser(ctx iris.Context) {
    id, ok := ctx.BindParam[int64]("id")
    if !ok {
        return
    }

    input, columns, ok := ctx.BindUpdate[UpdateUserInput]()
    if !ok {
        return // 400 already written: the body, or an unknown column.
    }

    updated, err := svc.UpdateUser(ctx, id, input, columns...)
    ctx.NoContent(updated, err) // 204, or 304 when nothing changed.
}
```

The body half is `BindJSON` exactly: the same decoder, the same
`RequestValidator` hook, the same `Validate` call. The columns arrive
either as a repeated key (`?columns=name&columns=email`) or as one
comma-separated value (`?columns=name,email`); both spellings are
accepted, values are trimmed, empties dropped and duplicates removed,
keeping the client's order. No columns at all means a full update and
yields a nil slice.

Every column is checked against the JSON member names of `T`, the `json`
tag name or the Go field name where there is no tag, and an unknown one
answers 400 naming the column and the names it could have been. The
comparison is exact where the body decoder is case-insensitive, because a
column name reaches an `UPDATE` statement: the endpoint must receive the
spelling it declared rather than a normalized guess. The names that come
back are still the client's, so a store that builds SQL from them maps
them to its own columns and never interpolates them.

## The Context Read Family

When the payload is not JSON, or when you want to handle the error
yourself instead of letting the helpers respond, the context offers a
`Read*` method per format. Each decodes into a pointer, using a struct
tag that matches the source:

| Method                      | Source                          | Struct tag     |
| --------------------------- | ------------------------------- | -------------- |
| `ctx.ReadJSON(&v, opts...)` | JSON body                       | `json`         |
| `ctx.ReadXML(&v)`           | XML body                        | `xml`          |
| `ctx.ReadYAML(&v)`          | YAML body                       | `yaml`         |
| `ctx.ReadMsgPack(&v)`       | MessagePack body                | —              |
| `ctx.ReadProtobuf(msg)`     | Protobuf body (`proto.Message`) | —              |
| `ctx.ReadForm(&v)`          | URL-encoded or multipart form   | `form`         |
| `ctx.ReadQuery(&v)`         | URL query                       | `url`          |
| `ctx.ReadParams(&v)`        | path parameters                 | `param`        |
| `ctx.ReadHeader(&v)`        | request headers                 | `header`       |
| `ctx.ReadURL(&v)`           | path parameters + query         | `param`, `url` |
| `ctx.ReadBody(&v)`          | dispatched by content type      | depends        |

XML and YAML are, like JSON, human-readable text formats with different
syntax. MessagePack and Protobuf are binary encodings: they trade
readability for smaller, faster payloads and are common in
service-to-service traffic rather than browser-facing APIs.

`ctx.ReadJSON` accepts optional `iris.JSONReader` options:

```go
var data map[string]any
err := ctx.ReadJSON(&data, iris.JSONReader{
    DisallowUnknownFields: true, // reject keys with no struct field.
})
```

Decoding runs on the standard library's `encoding/json/v2` (Go 1.27).
Two of its stricter rules apply to every reader: a body with duplicate
object keys or invalid UTF-8 is rejected (the bind helpers render that
as a 400). One rule is deliberately relaxed: member names still match
struct fields case-insensitively, as deployed clients expect from the
old `encoding/json`; set `JSONReader.CaseSensitive` to restore v2's
strict matching for a call. The `UnmarshalOptions` field takes raw
`encoding/json/v2` options, appended after the defaults so yours win:
`jsontext.AllowDuplicateNames(true)`, for example, accepts duplicate
members again.

For large arrays, `ctx.ReadJSONStream` decodes one object at a time
instead of buffering the whole body:

```go
var users []User
err := ctx.ReadJSONStream(func(decode iris.DecodeFunc) error {
    var u User
    if err := decode(&u); err != nil {
        return err
    }
    users = append(users, u)
    return nil
})
```

### ReadBody: content-type dispatch

`ctx.ReadBody(&v)` picks the decoder for you. For `GET` requests it
reads from the URL query (falling back to the form). For other methods
it switches on the request's media type: XML, YAML, url-encoded or
multipart forms, JSON, Protobuf and MessagePack are dispatched to the
matching `Read*` method; an unrecognized type falls back to the query
(when one exists) and finally to JSON.

\[Diagram: ctx.ReadBody short-circuits for raw string and byte targets, reads the query or the form for a GET, and otherwise switches on the media type, falling back to the query and then to JSON.]

```go
var raw []byte
if err := ctx.ReadBody(&raw); err != nil { // raw body, no decoding.
    errors.InvalidArgument.Err(ctx, err)
    return
}
```

The lowest level is `ctx.Body()`, which returns the body as `[]byte`.
Like any HTTP body, it can be consumed once: a later `ReadJSON` on the
same request finds it empty.

Empty-input behavior: `ReadForm`, `ReadQuery` and `ReadBody` return a
`nil` error on empty input by default, leaving the struct at its zero
value. Set `ContextOptions.FireEmptyFormError` to make them return
`iris.ErrEmptyForm` instead. `ReadForm` errors on unknown client fields;
ignore that class with `err != nil && !iris.IsErrPath(err)`.

## Pagination Input

Returning an entire table from one endpoint does not scale: the response
grows without bound and most clients display a screenful at a time.
Pagination splits the list into fixed-size pages the client requests one
by one; in database terms, the page number and size translate into how
many rows the query fetches and how many it skips first.

List endpoints usually accept `?page=&size=&details=` plus an optional
JSON filter body. `ctx.BindPagination` reads both at once:

```go
type BookFilter struct {
    Author string `json:"author"`
}

func listBooks(ctx iris.Context) {
    opts, filter, ok := ctx.BindPagination[BookFilter]()
    if !ok {
        return // error response already sent.
    }

    // opts.GetLimit(), opts.GetOffset() feed the database query.
    books, total, err := svc.Books(ctx, opts, filter)
    ctx.OK(iris.NewPage(opts, &filter, books, total), err)
}
```

`iris.PageOptions` carries `Page`, `Size` and `Details`; `GetLimit`
returns `Size` bounded by `pagination.MaxPageSize` (falling back to
`pagination.DefaultPageSize`, 100) and `GetOffset` derives the offset
from the page number. Set the two bounds once at startup, either
directly or through `iris.Patches().Context().SetMaxPageSize(n)` and
`SetDefaultPageSize(n)`. An empty body yields the zero filter. The
filter is optional by design.

A filter body on a read raises the question of which method carries it.
GET has no defined body semantics, and POST says the request changes
something, which it does not. HTTP QUERY (RFC 10008) is the method made
for this: safe and idempotent like GET, with a body like POST, and its
response is cacheable. Iris registers it with `app.Query`, so one path
can answer both shapes:

```go
app.Get("/books", listBooks)   // ?page=&size= only.
app.Query("/books", listBooks) // ?page=&size= plus a filter body.
```

Send the QUERY filter as JSON. On Go 1.27, net/http parses a
url-encoded body only for POST, PUT and PATCH, so a form-encoded QUERY
body never reaches `ctx.Form*`.

The page types themselves (`PageOptions`, `Page[T]`, `NewPage`,
`ListFilter`, `CountResponse[N]`) live in `core/pkg/pagination`, a
package that imports only the standard library. `iris` and `context`
alias them, so `iris.Page[T]` and `pagination.Page[T]` are the same
type, and the `client` package decodes a list into that type too.

`ctx.Paginated` collapses the whole flow (read options and filter, run
a collector, respond with a `iris.Page[T]`) into one call:

```go
import (
    "context"

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

func listBooks(ctx iris.Context) {
    ctx.Paginated(func(reqCtx context.Context,
        opts iris.PageOptions, filter BookFilter) ([]Book, int64, error) {
        return svc.Books(reqCtx, opts, filter)
    })
}
```

The response shape (`current_page`, `page_size`, `total_pages`,
`total_items`, `has_next_page`, `filter`, `items`) is covered in
[Chapter 6](https://iris-go.com/docs/response-handling/).

### The shared filter, and who chooses the sort

Most list filters carry the same four or five members before they carry
anything of their own. `iris.ListFilter` is that half, ready to embed:

```go
type BookFilter struct {
    iris.ListFilter

    Author string `json:"author,omitzero" url:"author"`
}
```

It fields `Term`, `Names`, `Substring`, `Sort` and `SortDescending`, and
carries both tag sets on purpose: the `json` names for a `QUERY` or
`POST` list body and the `url` names for a `GET` list route read
through `ReadQuery`. `f.SortColumn(allowed...)` returns the requested column or an error
wrapping `iris.ErrSortNotAllowed`, and `f.SortOrder()` returns `"ASC"` or
`"DESC"`.

A sort column reaches an `ORDER BY` clause, so which columns a list offers
belongs to the endpoint and never to the request. Two options say so
declaratively, and both answer 400 before the collector runs:

```go
func listBooks(ctx iris.Context) {
    ctx.Paginated(func(reqCtx context.Context,
        opts iris.PageOptions, filter BookFilter) ([]Book, int64, error) {
        return svc.Books(reqCtx, opts, filter)
    }, iris.AllowSort("title", "published_at"))
}
```

`iris.AllowSort(columns...)` limits the column a client may name, and the
rejection names both the column and the allowed ones.
`iris.DenySort(message)` refuses any sort at all, for a list whose order
the query itself fixes. Both work on `ctx.BindPagination` too, and both
read the decoded filter through its `SortRequest` method (the
`pagination.SortRequester` interface), which `ListFilter` provides, so a
filter without one is simply unaffected.

Sorting is often a permission rather than a shape. A list a client may
read but not order should answer 403, not 400, and any error code carries
the rejection:

```go
list, filter, ok := ctx.BindPagination[BookFilter](
    errors.PermissionDenied.DenySort("sorting is not available on this plan"))
if !ok {
    return
}
```

`errors.ErrorCodeName.DenySort`, from `rest/errors`, is `iris.DenySort`
answered with that code and is otherwise identical.

A collector may take its filter by pointer, which is the shape a
repository method already has, so it can be passed as a bare method value:

```go
ctx.Paginated(svc.ListBooksPaginated) // (context.Context, iris.PageOptions, *BookFilter) ([]Book, int64, error)
```

Such a collector never receives nil. A request with no body decodes to the
filter's zero value, so the repository reads an empty filter and the sort
policy still has a real `SortRequest` to read.

## Forms and File Uploads

### Form values

HTML form submissions (`application/x-www-form-urlencoded` or
`multipart/form-data`) bind through the `form` tag:

```go
type ContactForm struct {
    Name    string `form:"name"`
    Email   string `form:"email"`
    Message string `form:"message"`
}

func contact(ctx iris.Context) {
    var form ContactForm
    if err := ctx.ReadForm(&form); err != nil && !iris.IsErrPath(err) {
        errors.InvalidArgument.Err(ctx, err)
        return
    }
    // ...
}
```

Individual values are available through `ctx.Form()` (query + body
values) and `ctx.PostForm()` (body values only), which return the same
read-only entries view as `ctx.Query()`: `GetString`, `GetInt`,
`GetSlice` and friends all apply.

Multipart form parsing buffers up to `ContextOptions.PostMaxMemory`
in memory (default 32 MB, spillover goes to temporary files). Raise or
lower it through `iris.Options{Context: iris.ContextOptions{...}}`.

### File uploads

An uploaded file's name is client input like any other. A malicious
client can submit a name such as `../../etc/crontab`, hoping the server
joins it to the upload directory and writes outside it, an attack known
as path traversal. Sanitizing the name before it touches the filesystem
is therefore not optional, and the helper below does it for you.

`ctx.UploadFormFiles` is the batteries-included path: it saves every
uploaded file into a destination directory, sanitizes file names against
an allowlist pattern and blocks path-traversal attempts (unsafe names
are silently skipped). The optional callback filters or renames files
before saving:

```go
import (
    "mime/multipart"

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

func upload(ctx iris.Context) {
    files, total, err := ctx.UploadFormFiles("./uploads",
        func(ctx iris.Context, file *multipart.FileHeader) bool {
            return file.Size <= 8*iris.MB // reject anything larger.
        })
    if err != nil {
        errors.InvalidArgument.Err(ctx, err)
        return
    }

    ctx.JSON(iris.Map{"uploaded": len(files), "bytes": total})
}
```

For manual control, `ctx.FormFile(key)` returns the first uploaded file
for a form key and `ctx.FormFiles(key, ...)` all of them; pair either
with `ctx.SaveFormFile(header, dest)` to write to disk. When building
the destination path yourself, sanitize the client-supplied name first.
`context.SafeFilename` implements the same checks `UploadFormFiles`
uses: it strips every directory component, holds the result to an
allowlist of characters, rejects the reserved Windows device names
(`NUL`, `COM1`, `CON.txt` and the rest, which open a device rather than a
file), and resolves the destination directory before checking that the
final path is inside it. The upload endpoints are a natural place for the
body limits described below.

## Cookies

A cookie is a small named value the server asks the browser to store;
the browser then attaches it to every later request to the same site.
That echo is what gives stateless HTTP a memory: it is how a login
survives from one request to the next.

Two readers cover the request side:

```go
// Just the value; "" when the cookie is missing.
session := ctx.CookieValue("session_id")

// The full *http.Cookie; http.ErrNoCookie when missing.
cookie, err := ctx.Cookie("session_id")
if err == nil {
    _ = cookie.Value
}
```

`CookieValue` URL-unescapes the value for you. Setting, upserting and
removing cookies happens on the response and is covered in
[Chapter 6](https://iris-go.com/docs/response-handling/).

## Body Size Limits

Unbounded request bodies are a denial-of-service vector: a client can
stream gigabytes into `ReadJSON` or hide them behind a compressed body.
Iris offers an application-wide cap and a per-route middleware.

**Application-wide**: `ContextOptions.BodyMaxMemory` limits the total
bytes read by `Body`, `ReadJSON`, `ReadJSONStream` and
`ReadMultipartRelated`. That cap therefore also covers everything built
on them, including `ReadXML`, `ReadYAML`, `ReadBody` and `ctx.BindJSON`:

```go
app := iris.New(iris.Options{
    Context: iris.ContextOptions{
        BodyMaxMemory: 4 * iris.MB,
    },
})
```

The default `0` means no limit. Reading past the cap fails with
`context.ErrRequestEntityTooLarge` (package
`github.com/kataras/iris/v14/context`), which the `rest/errors` package
renders as a 413 `TOO_LARGE` response.

**Per route or group**: the `bodylimit` middleware applies the same
enforcement wherever it is registered, typically on upload routes:

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

media := app.Router("/media", bodylimit.New(20*iris.MB))
media.Post("/upload", upload)
```

It rejects requests up front when the declared `Content-Length` already
exceeds the limit, and wraps the body reader to catch chunked or
compressed bodies that only reveal their size while being read.

`PostMaxMemory` is a different knob: it bounds how much of a multipart
form is buffered in memory during parsing, not how large the body may
be. Use both where appropriate.

## The RequestValidator Hook

The per-type `Validate() error` method with `ctx.BindJSON` is the
recommended validation path. For teams standardizing on a tag-based
validation library instead, the context exposes one application-level
hook: `ContextOptions.RequestValidator`, any value implementing

```go
type Validator interface {
    Struct(any) error
}
```

It defaults to `nil`, so `validate:"..."` struct tags do nothing on their
own. When set, every decoding `Read*` method (`ReadJSON`, `ReadXML`,
`ReadYAML`, `ReadMsgPack`, `ReadForm`, `ReadQuery`, `ReadParams`,
`ReadURL`, `ReadHeader`, `ReadBody`) calls it after a successful decode
and returns its error. Wiring a third-party library is a three-line
adapter:

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

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

type structValidator struct{ v *validator.Validate }

func (s structValidator) Struct(ptr any) error { return s.v.Struct(ptr) }

func main() {
    app := iris.New(iris.Options{
        Context: iris.ContextOptions{
            RequestValidator: structValidator{validator.New()},
        },
    })
    // ...
}
```

The two mechanisms compose: `ctx.BindJSON` surfaces `RequestValidator`
failures through the same structured 400 responses it uses for a failed
`Validate()` method, so handlers keep the single `if !ok { return }`
guard either way.

## Summary

- `ctx.Method()`, `ctx.Path()`, `ctx.Host()` and friends expose request
  metadata; `ctx.RemoteAddr()` trusts proxy headers only when
  `RemoteAddrHeaders` is configured.
- `ctx.Header(name)` reads a request header (repeated values joined by
  `", "`); `ctx.ContentType()` / `ctx.MediaType()` describe the request
  body's declared type.
- `ctx.Params()` and `ctx.Query()` share typed accessors with fixed
  failure defaults (`GetString` → `""`, `GetInt` → `-1`); `Lookup` and
  `Exists` distinguish missing from zero; `GetSlice` returns raw
  multi-values.
- `ctx.BindJSON[T]()` is the recommended body reader: it decodes, runs
  the type's `Validate() error` automatically, and writes structured
  4xx responses. `ctx.BindJSONOptional[T]()` accepts an empty body.
- The `ctx.Read*` family covers JSON, XML, YAML, MessagePack, Protobuf,
  forms, query, params and headers; `ctx.ReadBody` dispatches by
  content type and fills `*string`/`*[]byte` with the raw body.
- `ctx.BindPagination` and `ctx.Paginated` turn `?page=&size=` plus an
  optional filter body into a complete list endpoint.
- `ctx.UploadFormFiles` saves multipart uploads with sanitized names;
  `PostMaxMemory` bounds form buffering, `BodyMaxMemory` and the
  `bodylimit` middleware cap body size (413 on overflow).
- `ContextOptions.RequestValidator` is the app-wide hook for tag-based
  validation libraries; without it, `validate` tags are inert.

## Further Reading

- [MDN: HTTP messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages).
  The anatomy of requests and responses: start line, headers, body.
- [MDN: MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types):
  how media types like `application/json` and `multipart/form-data`
  work.
- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110):
  the authoritative definition of methods, headers and message bodies.
- [RFC 8259: JSON](https://www.rfc-editor.org/rfc/rfc8259): the
  complete JSON grammar; shorter than most tutorials about it.
- [OWASP: Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html):
  server-side validation strategy, allowlists and common pitfalls.
- [OWASP: File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html):
  the risks behind upload endpoints and the checks that mitigate them.
