<!--
  Response Handling
  From the Iris book, chapter 6, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/response-handling/
-->
# Chapter 6: Response Handling

Iris gives you two layers for writing responses. The response helpers on
the context are the recommended surface for JSON APIs: each takes the value
and the error your service returned and decides what to send, including the
error response when something went wrong. Underneath sit the `Context`
writers (JSON, XML, YAML, text, files, streams) for the cases where you need
full control over the bytes on the wire. This chapter covers both layers, plus status codes,
response headers, redirects, streaming, Server-Sent Events, content
negotiation and the handler that runs when a response write itself fails.

## Background

An HTTP response mirrors the request's shape. Its first line (the
status line) carries a three-digit status code and a short reason
phrase ("200 OK", "404 Not Found"). Headers follow, describing what the
body is (`Content-Type`), how large it is (`Content-Length`) and how it
may be cached (`Cache-Control`). The body (HTML, JSON, file bytes)
comes last, and some responses legitimately have none.

Status codes are grouped by their first digit, and the class alone tells
a client how to react:

- **2xx**: success. 200 carries a body, 201 announces a created
  resource, 204 succeeds with nothing to return.
- **3xx**: redirection. The resource is elsewhere, or (304) the copy
  the client already holds is still fresh.
- **4xx**: the client made a mistake. Malformed input (400), missing
  credentials (401), no permission (403), no such resource (404).
- **5xx**: the server failed. The request itself may have been
  perfectly valid.

Treat the code you send as an API contract, because software you do not
control acts on it. Browsers follow 3xx redirects on their own. Proxies
and CDNs cache responses based on the code and headers: a 200 may be
stored and replayed, a 500 will not be. HTTP clients and load balancers
retry on some codes and give up on others. Monitoring counts 5xx
responses to decide whether to page someone. Send a 200 with an error
message hidden in the body and every one of those layers draws the
wrong conclusion.

Content negotiation is the name for "one URL, several formats". The
client's `Accept` request header lists the representations it can handle
(`application/json`, `text/xml`, ...) with optional preference weights;
the server compares that list against what it can produce and picks the
best match, or answers 406 Not Acceptable when there is none. The same
mechanism extends to compression (`Accept-Encoding`) and language
(`Accept-Language`).

What this chapter's helpers remove from your code is the ceremony
around those rules. Picking the right status for success and failure,
shaping a consistent error body, remembering that headers must precede
the first body byte. The response helpers encode those decisions once, so
a typical handler ends with a single line instead of a block of status
bookkeeping.

## The Response Helpers

In a typical handler the response is the last line: call the service, then
hand the result and the error to one helper. Every helper follows the same
contract:

```go
ok := ctx.Helper(value, err) // ok == false: an error response was written
```

If `err` is non-nil, the helper routes it through the central error mapping
of `rest/errors` (see [Chapter 9](https://iris-go.com/docs/error-handling/)) and a structured
error response is written. Your handler never branches on the error.
Otherwise the helper renders the success response. The returned `bool` is
`true` on success; you only check it when more work follows the write.

```go
package main

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

type ArticlesAPI struct {
    svc *ArticleService // your application's service layer.
}

func NewArticlesAPI(svc *ArticleService) *ArticlesAPI {
    return &ArticlesAPI{svc: svc}
}

func (api *ArticlesAPI) Routing(r iris.Router) {
    r.Get("/", api.list)
    r.Post("/", api.create)
    r.Get("/{id:int64}", api.get)
    r.Put("/{id:int64}", api.update)
    r.Delete("/{id:int64}", api.delete)
}

func (api *ArticlesAPI) get(ctx iris.Context) {
    id, ok := ctx.BindParam[int64]("id")
    if !ok {
        return // error response already written.
    }

    article, err := api.svc.ByID(ctx, id)
    ctx.OK(article, err)
}

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

    id, err := api.svc.Create(ctx, input)
    ctx.Created(id, err) // 201 with {"id": ...}.
}

func (api *ArticlesAPI) update(ctx iris.Context) {
    id, ok := ctx.BindParam[int64]("id")
    if !ok {
        return
    }

    input, ok := ctx.BindJSON[ArticleInput]()
    if !ok {
        return
    }

    updated, err := api.svc.Update(ctx, id, input)
    ctx.NoContent(updated, err) // 204, or 304 if nothing changed.
}

func (api *ArticlesAPI) delete(ctx iris.Context) {
    id, ok := ctx.BindParam[int64]("id")
    if !ok {
        return
    }

    deleted, err := api.svc.Delete(ctx, id)
    ctx.NoContent(deleted, err)
}
```

The full set of response helpers:

| Helper                              | Success status         | Renders                                                                  |
| ----------------------------------- | ---------------------- | ------------------------------------------------------------------------ |
| `ctx.OK(resp, err)`                 | 200 (status untouched) | `resp` as JSON; a nil `resp` sends nothing                               |
| `ctx.Created(resp, err)`            | 201                    | id scalars wrapped as `{"id": ...}`, other values as-is                  |
| `ctx.NoContent(modified, err)`      | 204 or 304             | empty body: 204 when `modified` is true, 304 otherwise                   |
| `ctx.Count(rowsAffected, err)`      | 200                    | `{"count": N}`; zero is a valid answer, a negative count renders as zero |
| `ctx.List(items, err, tolerate...)` | 200                    | `items` as a JSON array; empty and nil both render `[]`                  |
| `ctx.JSONOrError(resp)`             | current                | `resp` as JSON, with encode failures handled for you                     |
| `ctx.Paginated(collect)`            | 200                    | the standard page envelope                                               |

Two of the family are generic: `ctx.Count` infers the count's numeric
type from its argument, and `ctx.Paginated` infers filter, item and count
types from the collector you pass. Both follow the same contract as the
others: the error routes through the central mapping, and `false` means a
response was written.

`ctx.JSONOrError` is the low-level member of the family: it has no error
argument, it simply calls `ctx.JSON` and converts an encoding failure into a
proper error response instead of a broken body. Reach for it when the status
code or the value needs preparing first.

### What Created renders

`ctx.Created` recognizes identifier values and wraps them in a JSON object,
so a create handler can return the new id directly from the service:

```go
type IDType interface {
    string | int | int64 | uint64 | uuid.UUID
}

type IDPayload[T IDType] struct {
    ID T `json:"id"`
}
```

A `string`, `int`, `int64`, `uint64` or `uuid.UUID` result becomes
`{"id": ...}` (a UUID is rendered as its string form). The `uuid.UUID`
here is the standard library's `uuid` package type (Go 1.27), the same
one route macros and the book's examples use. A `[]string` whose
elements are UUIDs becomes a JSON array of `{"id": ...}` objects, useful
for bulk inserts. Any other value, such as the full created entity, is sent
as-is. A nil value sends the bare 201.

### What List renders

A list endpoint that is not paginated has two shapes to get right, and
hand-written versions get both wrong. A nil slice marshals as `null`
rather than `[]`, which every client has to special-case; and a store that
reports "not found" for an empty result set turns an ordinary empty list
into a 404. `ctx.List` answers both:

```go
func (api *ArticlesAPI) list(ctx iris.Context) {
    articles, err := api.svc.All(ctx)
    ctx.List(articles, err, sql.ErrNoRows) // 200 [] when there is nothing.
}
```

A nil or empty slice renders `[]`, never `null`, so the client can always
iterate the answer. An error matching one of the tolerated sentinels
(`errors.Is`, so a wrapped one matches) renders `[]` as well: a
repository's not-found for an empty set is not a failure of the list
endpoint. Any other error goes through the central error mapping and
`List` reports `false`, exactly like the rest of the family. Passing
something that is neither a slice nor an array is a programmer error and
panics naming the type, rather than rendering a JSON object where the
client expects an array.

Reach for `ctx.Paginated` instead as soon as the list can grow: `List`
sends the whole set.

### Pagination in one call

`ctx.Paginated` runs the complete listing flow: it reads `?page=` and
`?size=` from the query and an optional JSON filter object from the request
body, calls your collector with the parsed options, and renders the page
envelope.

```go
import (
    stdContext "context"

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

func (api *ArticlesAPI) list(ctx iris.Context) {
    ctx.Paginated(func(reqCtx stdContext.Context,
        opts iris.PageOptions, filter ArticleFilter) ([]Article, int64, error) {
        return api.svc.List(reqCtx, opts, filter)
    })
}
```

The collector returns the requested window of items and the total row count;
everything else (limit/offset math, the envelope, error handling) is done
for you. The response shape:

```json
{
    "current_page": 1,
    "page_size": 20,
    "total_pages": 3,
    "total_items": 42,
    "has_next_page": true,
    "filter": null,
    "items": [{"id": 1, "title": "First"}]
}
```

Request reading (`ctx.BindJSON`, validation, `ctx.BindParam`) is covered in
[Chapter 5](https://iris-go.com/docs/reading-requests/).

## Context Writers

When a helper does not fit (a non-JSON format, a custom shape, a template),
drop down to the `Context` writers. Each one sets the matching Content-Type
and writes the body:

| Method                 | Content-Type             | Options value   |
| ---------------------- | ------------------------ | --------------- |
| `ctx.JSON(v, ...)`     | application/json         | `iris.JSON`     |
| `ctx.JSONP(v, ...)`    | text/javascript          | `iris.JSONP`    |
| `ctx.XML(v, ...)`      | text/xml                 | `iris.XML`      |
| `ctx.YAML(v)`          | application/x-yaml       | —               |
| `ctx.TextYAML(v)`      | text/yaml                | —               |
| `ctx.Markdown(b, ...)` | text/html                | `iris.Markdown` |
| `ctx.Text(s)`          | text/plain               | —               |
| `ctx.HTML(s)`          | text/html                | —               |
| `ctx.Binary(b)`        | application/octet-stream | —               |

`JSON`, `JSONP`, `XML` and `Markdown` accept an optional options value as
their last argument; the rest take none. Every one of them returns a single
`error`, nil when the body was written.

JSONP is a legacy format: the JSON is wrapped in a JavaScript function
call so that browsers predating cross-site request rules could load it
through a `<script>` tag. New APIs rarely need it.

### JSON

```go
func report(ctx iris.Context) {
    data := iris.Map{"name": "Jane", "score": 9001}

    ctx.JSON(data, iris.JSON{
        Indent:         "  ",  // pretty-print with two spaces.
        EscapeHTML:     false, // true escapes <, >, & inside strings.
        Prefix:         "",    // optional string written before the body.
        EscapeNonASCII: false, // escape non-ASCII runes when true.
        ArrayPrefix:    false, // prepend "while(1);" to top-level arrays.
    })
}
```

Without options, `ctx.JSON(v)` streams the value with the default encoder,
which is the standard library's `encoding/json/v2` (Go 1.27). Two of its
defaults are worth knowing. HTML characters inside strings are *not*
escaped: responses are served as `application/json`, never inline HTML,
so the escaping buys nothing and costs bytes; set `EscapeHTML: true` to
restore the old `encoding/json` behavior (JSONP always escapes, because
there the payload does land in a script body). And nil slices and maps
render as `[]` and `{}` rather than `null`, so list endpoints never make
clients branch on `null`. `time.Duration` still encodes as int64
nanoseconds. For anything else the struct's `MarshalOptions` field
accepts raw `encoding/json/v2` options, appended after the framework
defaults so yours win. `iris.Map` is a shorthand for `map[string]any`.

Those framework defaults live in one place, `core/pkg/jsonutil`, and every
part of Iris that touches JSON composes its options from there: the context
writers and readers, the `rest/errors` renderers, the SSE and AI encoders,
the access log and the HTTP client. That is why a value encoded by the
client and decoded by a handler cannot disagree about what the wire looks
like.

### XML

Structs marshal through `encoding/xml`. Maps need the `iris.XMLMap` wrapper
because Go maps have no element name:

```go
ctx.XML(payload, iris.XML{Indent: "  "})
ctx.XML(iris.XMLMap("Root", iris.Map{"key": "value"}))
```

### Markdown

`ctx.Markdown` parses the given markdown bytes to HTML and serves the result
as text/html. Set `Sanitize` when the source is user-provided:

```go
ctx.Markdown([]byte("# Release Notes"), iris.Markdown{Sanitize: true})
```

### Raw writes

For output that needs no content-type handling, write directly:

```go
ctx.WriteString("plain body")
ctx.Writef("hello, %s", name)
ctx.Write([]byte{0xCA, 0xFE})
```

HTML template rendering goes through the `middleware/view` package
(`view.Handler` plus `view.Render`) and is covered in
[Chapter 12](https://iris-go.com/docs/built-in-middleware/).

## Status Codes

`ctx.StatusCode` sets the response status; `ctx.GetStatusCode` reads what has
been set so far. The status must be set before the first body write; writing
the body flushes the status line and headers to the wire:

```go
func createLegacy(ctx iris.Context) {
    ctx.StatusCode(iris.StatusCreated) // 201, before the body.
    ctx.JSON(iris.Map{"id": 42})
}
```

That handler is named for what it is. `ctx.Created(42, err)` from
[The Response Helpers](#the-response-helpers) above sets the same status,
wraps the same `{"id": 42}` and routes the error, so a real create endpoint
never sets a status by hand.

Iris exposes the full set of constants: `iris.StatusOK`,
`iris.StatusCreated`, `iris.StatusNoContent`, `iris.StatusBadRequest`,
`iris.StatusNotFound`, `iris.StatusInternalServerError` and the rest.
`iris.StatusText(code)` returns the standard reason phrase.

The `StopWith` family sets a status, writes an optional body and stops the
handler chain in one call (following `ctx.Next` calls are ignored):

| Method                              | Body                                              |
| ----------------------------------- | ------------------------------------------------- |
| `ctx.StopWithStatus(code)`          | none                                              |
| `ctx.StopWithText(code, text)`      | the given text                                    |
| `ctx.StopWithJSON(code, v)`         | `v` as JSON                                       |
| `ctx.StopWithError(code, err)`      | `err.Error()` as text; none when `err` is private |
| `ctx.StopWithPlainError(code, err)` | none; the error is stored for error handlers      |

`StopWithError` and `StopWithPlainError` also record the error through
`ctx.SetErr`, so middleware running after the handler can inspect it
with `ctx.GetErr`, or with `ctx.ErrPublic`, which reports whether the
stored error is safe to show the client and hands back the original
either way. If the error is private, because `iris.PrivateError` or
`errors.Private` marked it or it wraps such an error anywhere in its
chain, `StopWithError` withholds the message, sends only the status and
stores the error as private. The "none" in the table is what the method
writes. On an application built with `iris.NewBuilder()` the response
does not stay empty: `errors.Handler` fills a failure status that nobody
gave a body to with the JSON error envelope after the chain returns. For
API error responses prefer the structured senders of `rest/errors`
([Chapter 9](https://iris-go.com/docs/error-handling/)); the `StopWith` methods are the
low-level tools they are built on.

## Response Headers

`ctx.SetHeader` writes a response header; an empty value deletes it. Request
and response access are separate methods, a common source of confusion:

| Read the request      | Read the response             |
| --------------------- | ----------------------------- |
| `ctx.Header("X-Key")` | `ctx.ResponseHeader("X-Key")` |
| `ctx.ContentType()`   | `ctx.ResponseContentType()`   |
| `ctx.MediaType()`     | `ctx.ResponseMediaType()`     |

```go
func cached(ctx iris.Context) {
    ctx.SetHeader("Cache-Control", "public, max-age=3600")
    ctx.SetHeader("X-API-Version", "1")

    ctx.JSON(iris.Map{"ok": true})

    // Reads back what this handler set, not what the client sent:
    _ = ctx.ResponseHeader("Cache-Control") // "public, max-age=3600"
    _ = ctx.ResponseMediaType()             // "application/json"
}
```

`SetHeader` replaces any value already set for that name, so a middleware
registered twice cannot emit the header twice. When a list is what you mean,
`ctx.AddHeader` appends instead, and the underlying header map is always
there for anything else:

```go
ctx.AddHeader("Vary", "Accept-Encoding")
ctx.AddHeader("Vary", "Accept-Language")

ctx.ResponseWriter().Header().Set("X-Custom", "only-value")
ctx.ResponseWriter().Header().Add("Link", "</style.css>; rel=preload")
```

`ctx.SetContentType` sets the Content-Type and returns an error for invalid
values; the writers above call it for you, so you only need it with raw
writes. Like the status code, headers must be in place before the first body
byte; anything set afterwards is silently dropped by `net/http`.

## Redirects

A redirect is a response with a 3xx status and a `Location` header
naming another URL; a browser (or any client configured to follow
redirects) reacts by requesting that URL instead. The code chooses the
semantics: 301 and 308 mean "moved for good, update your links" and may
be cached by browsers for a long time, while 302, 303 and 307 mean "go
here this time".

`ctx.Redirect` sends a redirect and stops the handler chain. The status code
argument is optional and defaults to 302 (`iris.StatusFound`):

```go
func oldEndpoint(ctx iris.Context) {
    ctx.Redirect("/v2/endpoint", iris.StatusMovedPermanently) // 301
}

func afterLogin(ctx iris.Context) {
    ctx.Redirect("/dashboard") // 302 by default.
}
```

Use `iris.StatusTemporaryRedirect` (307) or `iris.StatusPermanentRedirect`
(308) when the client must replay the request with the same method and body,
and `iris.StatusSeeOther` (303) to turn a POST into a follow-up GET. A
non-3xx status set earlier on the context is ignored: redirect responses
are always sent with a redirect code.

## File Responses

Three methods cover files, differing in where the content comes from and how
the browser treats it:

```go
// Inline: render in the browser (assets, images, PDFs to display).
func manual(ctx iris.Context) {
    if err := ctx.ServeFile("./documents/manual.pdf"); err != nil {
        return // 404, 403 or 500 status already set.
    }
}

// Attachment: force a download dialog with the given file name.
func export(ctx iris.Context) {
    ctx.SendFile("./exports/report-2026.csv", "report.csv")
}

// From memory or any io.ReadSeeker, with a modification time.
func generated(ctx iris.Context) {
    content := bytes.NewReader(pdfBytes)
    ctx.ServeContent(content, "invoice.pdf", time.Now())
}
```

`SendFile` is `ServeFile` plus a `Content-Disposition: attachment` header; it
derives the download name from the source path when the second argument is
empty. `ServeContent` delegates to `http.ServeContent`, so Range requests,
`If-Modified-Since` and ETag-based conditional requests work out of the box.

Every method has a rate-limited variant that throttles the transfer.
`limit` is bytes per second, `burst` the bucket size in bytes:

```go
ctx.ServeFileWithRate("./files/video.mp4", 1024*1024, 64*1024) // ~1 MB/s
ctx.SendFileWithRate("./files/big.zip", "big.zip", 256*1024, 32*1024)
ctx.ServeContentWithRate(content, "data.bin", modTime, 512*1024, 32*1024)
```

Conditional requests are how HTTP avoids resending unchanged content:
the client remembers a fingerprint of the copy it holds (its
modification time, or an ETag, an opaque version tag chosen by the
server) and sends it back on the next request, meaning "only send the
body if this changed". When nothing changed, the server answers 304 Not
Modified with an empty body and the client keeps its copy. Range
requests are the related trick for large files: the client asks for a
byte slice instead of the whole file, which is what makes download
resumption and video seeking work.

For conditional non-file responses, the same primitives `http.ServeContent`
uses are available directly: `ctx.SetLastModified(t)` sets the header,
`ctx.CheckIfModifiedSince(t)` compares against the request,
`ctx.WriteNotModified()` sends a 304, and `ctx.WriteWithExpiration(body, t)`
combines all three into one call.

Serving whole directories is the job of the `controller/fileserver`
package, covered with its caching options in
[Chapter 14](https://iris-go.com/docs/caching-strategies/).

## Streaming

Normally the server buffers a response and sends it when the handler
returns. Flushing changes that: each chunk is pushed to the client the
moment it is written, turning the connection into a live feed.

For a response produced over time (a progress feed, a large export), write
in chunks and flush each one to the client:

```go
func progress(ctx iris.Context) {
    ctx.SetContentType("text/plain")

    for i := 1; i <= 5; i++ {
        ctx.Writef("step %d done\n", i)
        ctx.ResponseWriter().Flush() // push the chunk now.
        time.Sleep(time.Second)
    }
}
```

`ctx.StreamWriter` packages the same loop: it calls your function,
flushes, and calls it again until you return a non-nil error or the client
goes away. Return `nil` to continue with the next chunk; return an error
(`io.EOF` is the conventional "nothing more to send") to end the stream:

```go
import (
    "fmt"
    "io"
    "time"

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

func countdown(ctx iris.Context) {
    ctx.SetContentType("text/plain")

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    left := 10
    ctx.StreamWriter(func(w io.Writer) error {
        if left == 0 {
            return io.EOF // any non-nil error ends the stream.
        }

        <-ticker.C
        left--
        _, err := fmt.Fprintf(w, "%d seconds left\n", left)
        return err // nil: flush this chunk and call me again.
    })
}
```

When the client disconnects, `StreamWriter` returns the request context's
cancellation error, so the goroutine does not leak. In hand-rolled loops,
check `ctx.IsCanceled()` between chunks for the same guarantee.

`StreamWriter` is a poll loop: the framework asks for the next chunk when
it is ready for one. That suits a writer which blocks until it has
something to say (a ticker, a queue read, a database cursor) and fits
badly when the events arrive from a producer goroutine on their own
schedule. For Server-Sent Events, the next section is the better path in
both shapes.

One trap applies to every stream, whichever way you write it. Two of the
bundled middleware buffer the body: `middleware/compress` writes into a
compressor, and `middleware/recorder` writes into a `bytes.Buffer`, which
`accesslog` installs whenever it reads the response body, and which
`iris.NewBuilder().LogRequests(true)` therefore turns on. Neither forwards
anything when you flush, so the handler streams into a buffer and the
client sees one lump on return. `ctx.DisableResponseBuffering()` removes
those wrappers for the current response, before the first write, and
returns an error naming any writer that refused to step aside. The `sse`
package calls it for you.

## Server-Sent Events

Server-Sent Events (SSE) is a one-way stream of text frames sent over an
ordinary HTTP response. A frame is a group of `field: value` lines ended
by a blank line, and a line beginning with a colon is a comment:

```text
event: token
data: Hello

: keep-alive

```

Four fields are defined. `event` names the event and, when absent, the
client receives it as `message`; `data` carries the payload; `id` becomes
the client's resume cursor; `retry` sets how long the client waits before
reconnecting. Browsers consume the format natively through `EventSource`,
which reconnects on its own and sends the last id it saw back in a
`Last-Event-ID` header. Everything else is ordinary HTTP: status codes,
authorization headers and proxies all behave as they do for any other
response.

Writing those frames by hand is two lines of `fmt.Fprintf`, which is why
so many services do, and why so many of them fail in the same four places.
A buffering middleware holds the frames. The server's absolute write
timeout ends the response mid-answer. A reverse proxy buffers it anyway.
And a client that vanishes without closing its connection is never
noticed, so the work behind the stream runs on for nobody. The `sse`
package exists for those four, and the rest of this section is what it
does about them.

### Opening a stream

`sse.Open` prepares the response and writes nothing:

```go
import (
    "fmt"
    "time"

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

func events(ctx iris.Context) {
    stream, err := sse.Open(ctx)
    if errors.HandleError(ctx, err) {
        return // ErrNotStreamable renders as a 500; nothing has been written.
    }
    // Close writes the terminal frame, on success as well as on failure.
    defer stream.Close(nil)

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for i := 1; i <= 10; i++ {
        select {
        case <-ctx.ShuttingDown():
            return // the server is stopping: end while we still can.
        case <-stream.Context().Done():
            return // the client is gone.
        case <-ticker.C:
        }

        if err := stream.Send(sse.Event{
            Name: "tick",
            Data: fmt.Appendf(nil, "tick %d", i),
        }); err != nil {
            return
        }
    }
}
```

That `Open` writes nothing is the property the rest of the design rests
on. The protocol headers commit on the first `Send`, so a check that runs
after opening can still refuse the request properly, with a status code
and a body:

```go
func guarded(ctx iris.Context) {
    stream, err := sse.Open(ctx)
    if errors.HandleError(ctx, err) {
        return
    }
    defer stream.Close(nil)

    if ctx.Query().Get("token") == "" {
        // A real status code and a real body: not one byte has gone out.
        errors.Unauthenticated.Message(ctx, "token required")
        return
    }

    stream.Send(sse.Event{Name: "welcome", Data: []byte("you are in")})
}
```

A handler that opens a stream and returns without sending anything leaves
the response untouched as well, which is what lets an error handler
further up the chain render it.

`Open` does check one thing up front: whether this response can flush at
all. It cannot under `Timeout.HandlerTimeout`, which wraps the router in
`http.TimeoutHandler`, whose response writer implements neither
`http.Flusher` nor `http.Hijacker`. Every write buffers and the client
receives a 503 page instead of a stream. Because the answer is otherwise
only available at flush time, which is far too late, `Open` reports
`sse.ErrNotStreamable` with zero bytes written and the handler can still
answer honestly. Setting `HandlerTimeout` on an application that streams
anything logs a warning at startup for the same reason.

On the first `Send`, and for that one response, five adjustments land that
no middleware could guess were wanted:

- every buffering response-writer wrapper is removed, so compression and
  the recorder that `accesslog` installs stop holding the events;
- the server's absolute `WriteTimeout` is cleared. It is a total deadline
  rather than an idle one, so it would otherwise end a long answer
  mid-sentence;
- full duplex is enabled on HTTP/1, so a request body can still be read
  after the response has started, which is how a POST-driven stream is
  called;
- `Cache-Control: no-cache, no-transform` and `X-Accel-Buffering: no` are
  set and any `Content-Length` is deleted, which is what stops nginx and
  the CDNs in front of it from buffering the stream into one response;
- `Connection: keep-alive` is set on HTTP/1 only, where it is meaningful.
  It is a protocol error on HTTP/2.

### Sending events

`Send` writes one frame and flushes it. `SendJSON(name, v)` marshals a
value into the data field, and `Comment` writes a comment-only frame. All
of them are safe to call from more than one goroutine: the heartbeat takes
the same lock, so a keep-alive can never interleave with a half-written
frame.

`sse.Event` is the frame itself, and a zero field is simply absent from
the wire:

| Field     | Wire field | Notes                                                                                             |
| --------- | ---------- | ------------------------------------------------------------------------------------------------- |
| `ID`      | `id`       | Becomes the client's `Last-Event-ID`. Stamped for you while `AutoID` is on                        |
| `Name`    | `event`    | Empty means `message`, which is what an `EventSource` `onmessage` handler receives                |
| `Data`    | `data`     | A multi-line value is split across several `data` fields and rejoined by the client with newlines |
| `Retry`   | `retry`    | The client's reconnection delay, written once with the first event                                |
| `Comment` | `: `       | Written before the fields. A nil `Data` with a comment dispatches no event at all                 |

`ID`, `Name` and `Comment` are rejected with `sse.ErrInvalidField` when
they contain CR, LF or NUL. Those values are terminated by a newline on
the wire, so without the check a caller-supplied id forges frames of its
own.

### Closing, and the frame that says so

`Close` writes a final `done` event and releases the stream. It is
idempotent, so a deferred `Close(nil)` and an explicit `Close(err)`
coexist, and a panicking handler still terminates the stream cleanly.

It writes that frame on success too, and that is the point. If the
terminator is always present, its absence is how a client detects
truncation. Nothing below it offers that signal: chunked framing catches a
torn connection, but a proxy idle timeout closes the stream cleanly and
the client cannot tell a finished answer from a severed one. A browser
client closes on it explicitly, since `EventSource` would otherwise
reconnect:

```js
const es = new EventSource("/events");
es.addEventListener("tick", (e) => console.log(e.data));
es.addEventListener("done", () => es.close()); // the terminal frame.
```

When `Close` is given an error and nothing has been committed, it writes
nothing and returns the error, so the caller renders an ordinary HTTP
response. After the first `Send` it writes an error event and then the
terminator, because the status code is already spent. `stream.Committed()`
reports which side of that line you are on, and `stream.Err()` reports why
a stream ended.

What goes into that error event is the `ErrorFrame` option, and its
default is the reason a failed stream is not a special case.
`sse.ReportErrorFrame` runs the error through
[`errors.Report`](https://iris-go.com/docs/error-handling/), the `rest/errors` pipeline with
the rendering step removed: the route's error mappings apply, the logger
and the collector fire, a correlation id is generated, and an unmapped or
private error is redacted to a generic message before anything reaches the
client. The payload is the same `rest/errors` envelope an ordinary failure
would have produced, so one client parser handles a failure whether it
arrived before the first byte or after it.

The redaction is the part worth noticing. Half of what a streaming handler
can fail on is somebody else's text, and an upstream message naming a
vendor, a model or an internal host has no business on the wire.
`sse.RawErrorFrame` sends the error's own words instead, which is
appropriate only when you know where those words came from. A frame is
skipped entirely when there is nothing to report, which is the case for a
client that already disconnected while `errors.SkipCanceled` is on: there
is nobody left to tell.

### The options, and where their defaults come from

`sse.Open(ctx, opts)` takes an `sse.Options`. Start from
`sse.DefaultOptions()` and change what you need: a zero field means off
rather than default, so switching the heartbeat or the terminator off
stays expressible.

| Option           | Default                | What it is for                                                                                                                                                |
| ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Heartbeat`      | `15s`                  | The longest the stream may go without writing anything                                                                                                        |
| `HeartbeatFrame` | `": ping\n\n"`         | What the heartbeat writes. A comment, which conforming clients ignore                                                                                         |
| `Retry`          | `3s`                   | Advertised to the client once, with the first event                                                                                                           |
| `AutoID`         | `true`                 | Stamps an increasing id on every event that has none, so a gap is detectable within one connection. Not resumption: the counter restarts at 1 on every stream |
| `Terminator`     | `"done"`               | The event name `Close` writes                                                                                                                                 |
| `ErrorFrame`     | `sse.ReportErrorFrame` | Renders the error event `Close` writes when a committed stream failed                                                                                         |
| `SendTimeout`    | `30s`                  | A write deadline for one `Send`. Exceeding it fails the stream with `sse.ErrSlowClient`                                                                       |

Two of those defaults carry an argument worth reading. Fifteen seconds is
not a round number: it is the largest heartbeat that survives every common
intermediary at once, since nginx `proxy_read_timeout` and an AWS
Application Load Balancer both idle out at 60 seconds, Cloudflare returns
a 524 at 100, and Heroku's router applies a rolling 55. The heartbeat's
larger job is liveness rather than keep-alive: only a real FIN or RST, or
a failing write, cancels a request context, so a client that disappears
without one (a closed lid, a dropped link, an evicted NAT entry) stays
invisible until TCP gives up, which on Linux is roughly 924 seconds of
leaked goroutine and leaked upstream work. Writing something is what lets
the kernel fail and the handler find out. A Google Cloud load balancer is
the exception no heartbeat rescues, because it applies its backend timeout
to the whole request and response; raise `timeoutSec` there instead.

`SendTimeout` is the backpressure mechanism, and it is a deadline rather
than a queue on purpose. `Send` is synchronous, so there is no buffer of
the framework's to budget, and a client that will not drain shows up as a
write that does not complete. Exceeding it fails the stream and never
skips an event, because a dropped delta corrupts the message the client is
assembling with no way for it to notice.

`HeartbeatFrame` is configurable rather than fixed because not every
client conforms. `sashabaranov/go-openai`, widely used against
OpenAI-compatible servers, counts comment frames toward its
`emptyMessagesLimit` and fails the stream instead of ignoring them.
Serving such a client, send a real event.

### Batching deltas

A fast producer is a syscall amplifier. One frame per token means one
write, one flush, one TCP segment and one client-side render per token,
and the tokens arrive faster than any of those is worth doing. `Coalesce`
batches the events you name and emits at most one frame per name per
interval, concatenating their payloads:

```go
batch := stream.Coalesce(200*time.Millisecond, "word")
defer batch.Close()

for _, word := range strings.Fields("the quick brown fox") {
    ev := sse.Event{Name: "word", Data: []byte(word + " ")}
    if err := batch.Send(ev); err != nil {
        return
    }

    time.Sleep(80 * time.Millisecond)
}
```

A background ticker drains the buffers, which is what makes the interval a
ceiling rather than a floor. Without one, a buffered delta is only emitted
when the next one arrives, so text that already reached the server sits
unsent through exactly the mid-answer pause a reader notices.

Because the payloads are concatenated, this is for text-like deltas
(running prose, a reasoning trace) and not for events whose data is a
self-contained JSON document. Anything not named passes straight through,
after the pending buffers are flushed, so ordering holds. The coalescer
owns the stream while it is open and `Send` on the stream itself flushes
it first, so the interleaving hazard a hand-rolled batcher has to document
cannot be expressed here.

### Resuming after a reconnect

`stream.LastEventID()` reports the cursor the client sent, from the
`Last-Event-ID` header or the `lastEventId` query parameter. Both are
load-bearing: a browser `EventSource` sends the header and cannot be made
to send anything else, while a client built on `fetch` usually has to put
it in the URL. The value is sanitised, because it is echoed back on every
frame.

Where the events between that cursor and now come from is the
application's business. The package stores nothing, which is what keeps it
small enough to be correct: a replay buffer belongs where the data already
lives.

### Reading somebody else's stream

`sse.Read` is the other half, for consuming an event stream from an
upstream service. It is the framework's first `iter.Seq2` API, so it
ranges:

```go
resp, err := httpClient.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

for ev, err := range sse.Read(resp.Body, sse.ReadOptions{
    Terminators: []string{"[DONE]"},
}) {
    if err != nil {
        return err
    }

    fmt.Println(ev.Name, string(ev.Data))
}
```

It implements the parsing algorithm rather than matching a prefix, which
matters more than it sounds: `data:` with and without a space are both
legal, and a reader that matches `data: ` sees an empty stream from a
producer that omits it. Multi-line data, comments, unknown fields, a
leading byte order mark and lines ended by CR, LF or CRLF are all handled.
It never yields a partially framed event, because an event is dispatched
by the blank line that ends it; a stream cut mid-event ends the iteration
with `io.ErrUnexpectedEOF` rather than quietly, as if the answer were
complete. A clean end of stream simply ends the iteration, and `io.EOF` is
not reported as an error.

`ReadOptions.Terminators` ends the iteration on an in-band sentinel such
as `[DONE]`, which the OpenAI dialect and everything shaped after it emit.
It is a concession on the reading side and never something this package
writes, for the reason its own terminator is an event name: a sentinel
travels on the same channel as the payload, so a producer that
legitimately emits the literal string would end the stream.
`RequireTerminator` goes with it: when the upstream promised a sentinel
and closed without one, the iteration ends with `sse.ErrNoTerminator`,
because a stream that ends short was cut however clean the close looked to
the transport. `MaxEventBytes` caps one event, not the stream, at 1 MB by
default, and `KeepComments` yields an upstream's own heartbeats instead of
skipping them.

Relaying a model's stream is what the `ai` package is for, and it is built
on both halves of this one. See [Chapter 21](https://iris-go.com/docs/ai-backends/).

## Content Negotiation

When one endpoint must serve multiple representations, declare what the
server offers with `ctx.Negotiation()`, then let `ctx.Negotiate` pick the
best match for the request's `Accept` header:

```go
type User struct {
    ID   int    `json:"id" xml:"id"`
    Name string `json:"name" xml:"name"`
}

func user(ctx iris.Context) {
    // Server-side offers, in priority order.
    ctx.Negotiation().JSON().XML().Text()

    u := User{ID: 42, Name: "Jane"}
    ctx.Negotiate(iris.N{
        JSON: u,
        XML:  u,
        Text: fmt.Sprintf("user %d: %s", u.ID, u.Name),
    })
}
```

`iris.N` holds one value per offered representation (`Text`, `HTML`,
`Markdown`, `Binary`, `JSON`, `JSONP`, `XML`, `YAML`, `Protobuf`,
`MsgPack`, plus `Other` for custom types); the matched field is rendered
with the corresponding writer. If nothing matches, `Negotiate` sets 406 Not
Acceptable and returns `context.ErrContentNotSupported`. The builder can
also carry the content itself (`ctx.Negotiation().JSON(v).XML(v)` followed
by `ctx.Negotiate(nil)`) and offers `Charset(...)` and `EncodingGzip()`
to negotiate charset and compression alongside the media type.

## The Response Error Handler

The writers report failures (a value JSON cannot encode, a broken
connection) back to the caller as an `error`. In practice handlers do not
check that return value; instead, every failed write also invokes the
context error handler, configured at `Options.Context.ErrorHandler`. The
default logs the error in debug mode and replies with a bare 500.

Replace it application-wide through the options (partial options merge with
the defaults, see [Chapter 11](https://iris-go.com/docs/configuration-management/)):

```go
package main

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

func main() {
    app := iris.New(iris.Options{
        Context: iris.ContextOptions{
            ErrorHandler: func(ctx iris.Context, err error) {
                // Logs the write failure and answers a canonical 500; the
                // encoder's own message never reaches the client.
                errors.Internal.LogErr(ctx, err)
            },
        },
    })

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

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

For a single route or group, `iris.WithErrorHandler` builds a middleware that
overrides the handler for the requests it runs on. It calls `ctx.Next()`
itself, so register it like any other middleware and the chain carries on:

```go
app.Use(iris.WithErrorHandler(func(ctx iris.Context, err error) {
    errors.Internal.ErrPrivate(ctx, err)
}))
```

It works the same in front of a single route:

```go
app.Get("/report", iris.WithErrorHandler(reportErrors), reportHandler)
```

Applications built with `iris.NewBuilder` get
`errors.DefaultContextErrorHandler` from `rest/errors` installed
automatically, so write failures render as the same structured error JSON as
every other error in the app. Setting
`Options.Context.DisableErrorHandler: true` turns the mechanism off
entirely, leaving the returned `error` as the only signal.

## Summary

- The Context helpers are the recommended way to respond in JSON APIs: one
  contract, `ctx.Helper(value, err)`, with `ctx.OK`, `ctx.Created` (id
  values become `{"id": ...}`), `ctx.NoContent`, `ctx.JSONOrError`,
  `ctx.Count` and `ctx.Paginated`, covering the standard cases and routing
  every error through the central mapping.
- The `Context` writers handle the formats directly: `JSON`, `JSONP`, `XML`
  and `Markdown` accept an options value; `YAML`, `TextYAML`, `Text`,
  `HTML` and `Binary` take none.
- Set status codes with `StatusCode` before the body; the `StopWith` family
  combines status, body and stopping the chain.
- `SetHeader` replaces and `AddHeader` appends a response header, and
  `ResponseHeader` reads one back, mirroring `ctx.Header` for the request
  side; `ResponseContentType` and `ResponseMediaType` pair with the
  request's `ContentType` and `MediaType`.
- `Redirect` defaults to 302; `ServeFile`, `SendFile` and `ServeContent`
  cover inline files, downloads and in-memory content, each with a
  rate-limited variant.
- Stream with `Write` plus `ResponseWriter().Flush()`, or let
  `StreamWriter` run the flush loop; end the stream by returning an error.
  `ctx.DisableResponseBuffering()` removes the middleware wrappers that
  would otherwise hold the chunks.
- The `sse` package writes Server-Sent Events: `sse.Open` commits nothing
  until the first `Send`, so a check after it can still return a status
  code; that first `Send` unbuffers the response, clears the absolute
  `WriteTimeout` and sets the anti-buffering headers; `Close` writes the
  terminal frame on success too, so its absence means truncation.
  `Coalesce` batches deltas and `sse.Read` consumes an upstream stream.
- A failure after the first byte is reported in-band by the `ErrorFrame`
  option, whose default runs the error through the `rest/errors` pipeline,
  so it is mapped, logged, collected and redacted like any other failure.
- `Negotiation()` and `Negotiate(iris.N{...})` serve multiple
  representations from one endpoint.
- Failed writes trigger `Options.Context.ErrorHandler`;
  the `iris.WithErrorHandler` middleware overrides it for the routes it runs
  on, and builder apps default to the structured `rest/errors` handler.

## Further Reading

- [MDN: HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status):
  every status code with its intended use.
- [MDN: Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation):
  how `Accept` headers drive representation selection.
- [MDN: Redirections in HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections):
  the 3xx family and when to use each code.
- [MDN: HTTP conditional requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests):
  ETag, Last-Modified and the 304 mechanism in detail.
- [MDN: Using server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events):
  the SSE wire format and the browser-side `EventSource` API.
- [WHATWG HTML: Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html):
  the normative field list and the parsing algorithm `sse.Read` implements.
- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110):
  the authoritative source for status codes, headers and negotiation.
