# Response

> Return values that become responses: JSON, text, HTML, redirects, files and streams, with the status and headers you choose.

This guide covers building responses in Tinka. You will learn how to:

- Return values that become JSON, text or HTML responses
- Set the status code and response headers
- Redirect, also back to the previous page
- Set signed, encrypted and plain cookies
- Send files, downloads and streams
- Write the response yourself when you need to
- Create your own response types

## Overview

A handler returns its response as a value. The App writes it once every middleware has returned, so middleware can still set headers and the status, and a handler can never write two responses. A value is encoded as soon as the handler returns, so a value that can't be encoded becomes an ordinary error, never half a response.

The type a handler returns decides how its value is sent:

| Returned value | Sent as | Status |
|---|---|---|
| A struct, slice, map or number | `application/json` | `200` |
| A `string` | `text/plain; charset=utf-8` | `200` |
| `tinka.HTML` | `text/html; charset=utf-8` | `200` |
| `[]byte` | `application/octet-stream` | `200` |
| `nil` | no body | `204` |
| A `tinka.Response`, such as `tinka.Redirect` | what the Response writes | its own |

JSON keeps a struct's field order, sorts map keys, and encodes nil slices and maps as `[]` and `{}`, so clients never receive `null` for an empty list.

## Return JSON

Return the value. Handlers that always answer the same type declare it, so the compiler checks every return:

```go title="posts.go"
package main

import (
	"github.com/mucolabs/tinka"
)

type Post struct {
	ID    int64    `json:"id"`
	Title string   `json:"title"`
	Tags  []string `json:"tags"`
}

func showPost(c *tinka.Ctx) (Post, error) {
	return Post{ID: 1, Title: "Hello, Tinka"}, nil
}
```

```json title="Response"
{"id":1,"title":"Hello, Tinka","tags":[]}
```

## Set the status and headers

`c.Status` sets the status code, and `c.Header` returns the response's headers. Both can change until the response is written:

```go title="posts.go"
func createPost(c *tinka.Ctx) (Post, error) {
	post := Post{ID: 2, Title: "Second post"}
	c.Status(http.StatusCreated)
	c.Header().Set("Location", "/posts/2")
	return post, nil
}
```

A status set with `c.Status` wins over the default for the returned value, so a handler can answer `202 Accepted` with no body by setting the status and returning `nil`.

## Return text and HTML

A `string` is sent as plain text. A page is `tinka.HTML`, sent as `text/html`:

```go title="pages.go"
package main

import (
	"bytes"
	"html/template"

	"github.com/mucolabs/tinka"
)

var postPage = template.Must(template.New("post").Parse(`<h1>{{.Title}}</h1>`))

func showPostPage(c *tinka.Ctx) (tinka.HTML, error) {
	var page bytes.Buffer
	if err := postPage.Execute(&page, Post{Title: "Hello, <Tinka>"}); err != nil {
		return "", err
	}
	return tinka.HTML(page.String()), nil
}
```

:::warning
Build HTML with `html/template`, which escapes what it inserts. Text a user wrote, put into a `tinka.HTML` as it is, runs as markup in every visitor's browser: a comment containing `<script>` steals their sessions.

```go title="pages.go" del={1} ins={2-3}
	return tinka.HTML("<p>" + comment.Body + "</p>"), nil
	err := commentPage.Execute(&page, comment)
	return tinka.HTML(page.String()), err
```

A plain `string` is always sent as `text/plain`, so returning user text without `tinka.HTML` is safe.
:::

## Return different kinds of responses

A handler that answers in more than one form, such as a page for browsers and JSON for API clients, returns `tinka.Response`. `tinka.JSON` and `tinka.HTML` are both Responses:

```go title="posts.go"
func showPost(c *tinka.Ctx) (tinka.Response, error) {
	post := Post{ID: 1, Title: "Hello, Tinka"}
	if c.Accepts("json", "html") == "html" {
		return tinka.HTML("<h1>Hello, Tinka</h1>"), nil
	}
	return tinka.JSON(post), nil
}
```

## Redirect

`tinka.Redirect` sends the client to another URL, with `302 Found` unless `c.Status` set another code. Build the URL of a named route with [`URL`](url-builder.md):

```go title="posts.go"
func (h Posts) Create(c *tinka.Ctx) (tinka.Response, error) {
	post, err := h.Service.Create(c, "Second post")
	if err != nil {
		return nil, err
	}
	c.Status(http.StatusSeeOther)
	return tinka.Redirect(h.App.URL("posts.show", post.ID)), nil
}
```

`tinka.Back` sends the client back to the page it came from, by the `Referer` header, as a form handler does after a validation error. A referrer on another site, or none, sends it to the fallback you pass instead, so a link from elsewhere can't turn your App into an open redirect:

```go
	return tinka.Back("/posts"), nil
```

## Set cookies

`c.SetCookie` sets a **signed** cookie: the browser stores the value, encoded as JSON, and sends it back, but any change to it makes [`c.Cookie`](request.md#cookies) reject it. Signing needs the App's [Encrypter](../security/encryption.md), given in `HTTPConfig`:

```go title="main.go"
	enc, err := encryption.New(e.Secret("APP_KEY"))
	if err != nil {
		return err
	}
	app := tinka.New(tinka.HTTPConfig{Encrypter: enc})
```

```go title="cart.go"
type Cart struct {
	Items []string `json:"items"`
}

func addToCart(c *tinka.Ctx) (tinka.Response, error) {
	cart, _ := c.Cookie[Cart]("cart")
	cart.Items = append(cart.Items, c.Param("sku"))
	if err := c.SetCookie("cart", cart); err != nil {
		return nil, err
	}
	return tinka.Back("/cart"), nil
}
```

A signed cookie is readable by the client. When its value must stay secret, use `c.SetEncryptedCookie`, which the client can neither read nor change. When page scripts must read it, such as a theme, use `c.SetPlainCookie`, which stores a string as it is, escaped as `encodeURIComponent` does:

```go
	err := c.SetEncryptedCookie("onboarding", progress)
	err = c.SetPlainCookie("theme", "dark", tinka.ScriptReadable(), tinka.MaxAge(365*24*time.Hour))
```

Each value is bound to its cookie's name, so a value can't be moved to another cookie, and to its lifetime: a signed or encrypted cookie read after its `Max-Age` is reported missing, even when the client kept a copy. A cookie set with `tinka.MaxAge(0)` lasts until the browser closes, and has no lifetime on the server. Cookies are safe by default: they last 2 hours, travel over HTTPS only, are hidden from page scripts, and aren't sent on cross-site requests other than following a link (`SameSite=Lax`). `HTTPConfig.Cookie` changes the defaults for the App, and options change them for one cookie: `tinka.MaxAge`, `tinka.ScriptReadable`, or a function of your own that edits the `*http.Cookie`.

`c.ClearCookie` deletes a cookie. A cookie set with another path or domain needs the same options to be deleted.

`SetCookie` returns an error for a cookie larger than 4 KB, which browsers drop silently: keep large data on the server, and only an ID in the cookie.

:::note
Browsers send cookies marked `Secure` over HTTPS only. Most treat `http://localhost` as secure, so cookies work there during development. If your browser drops them on localhost, or you reach a development server by another name over plain HTTP, set `Insecure` in `HTTPConfig.Cookie` for development only.
:::

## Send files and streams

`tinka.SendFile` sends a file, with its `Content-Type` from its extension. It answers range requests, so videos seek and downloads resume, and `If-Modified-Since` requests with `304`. A missing file is a `404`. `tinka.Download` sends a file as an attachment the browser saves under the name you give:

```go title="invoices.go"
func downloadInvoice(c *tinka.Ctx) (tinka.Response, error) {
	id, err := c.ParamInt("id")
	if err != nil {
		return nil, err
	}
	path := filepath.Join("storage/invoices", strconv.FormatInt(id, 10)+".pdf")
	return tinka.Download(path, fmt.Sprintf("Invoice %d.pdf", id)), nil
}
```

:::warning
Never build a file path from request text without checking it. A path parameter such as `../../.env` would send your secrets. Parse the value into the type it should be, as `ParamInt` does above, or serve a directory with `http.FileServerFS`, which stays inside it.
:::

`tinka.Stream` sends what a reader reads, without holding it in memory, such as a large export or a body from another service. The reader is closed afterwards:

```go title="exports.go"
func exportPosts(c *tinka.Ctx) (tinka.Response, error) {
	csv, err := exports.Posts(c)
	if err != nil {
		return nil, err
	}
	c.Header().Set("Content-Disposition", `attachment; filename="posts.csv"`)
	return tinka.Stream("text/csv", csv), nil
}
```

## Write the response yourself

`c.ResponseWriter()` returns the `http.ResponseWriter`, for a response no value describes, such as Server-Sent Events. Once anything is written, the handler's returned value is ignored, so return `nil`:

```go title="events.go"
func streamEvents(c *tinka.Ctx) (tinka.Response, error) {
	w := c.ResponseWriter()
	w.Header().Set("Content-Type", "text/event-stream")
	rc := http.NewResponseController(w)
	for event := range events.Subscribe(c) {
		if _, err := fmt.Fprintf(w, "data: %s\n\n", event); err != nil {
			return nil, err
		}
		if err := rc.Flush(); err != nil {
			return nil, err
		}
	}
	return nil, nil
}
```

## Create a response type

A `tinka.Response` is any type with a `Respond(c *tinka.Ctx) error` method. The App calls it once every middleware has returned. It writes through `c.ResponseWriter()`, and uses the status set with `c.Status` when there is one:

```go title="csv.go"
package main

import (
	"encoding/csv"
	"net/http"

	"github.com/mucolabs/tinka"
)

// CSV is a table sent as text/csv.
type CSV [][]string

func (rows CSV) Respond(c *tinka.Ctx) error {
	w := c.ResponseWriter()
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	return csv.NewWriter(w).WriteAll(rows)
}
```

A handler returns it like any other value: `func listPosts(c *tinka.Ctx) (CSV, error)`. An error type with a `Respond` method renders itself the same way when a handler returns it, as the [Exception handling](exception-handling.md) guide shows.

See also: [HTTP context](http-context.md), [Request](request.md), [URL builder](url-builder.md), [Encryption](../security/encryption.md), [Exception handling](exception-handling.md)
