---
title: Controllers
description: How controllers turn a request into a response, from reading params and rendering templates to redirects, sessions, flash, cookies and errors.
category: Views and Controllers
order: 2
---
A controller is where a request is handled once the router has chosen it.
Its actions read what was sent, talk to models, and answer with a page, a
redirect, JSON or just a status. Glacier controllers are written in
`.glacier` and compiled to ordinary Rust when the app builds, so a typo in
a template prop or a model method is a build error.
After reading this guide, you will know:
- Where controllers live and how they're registered.
- What an action is, and what `ctx` tells it about the request.
- How to read params, including nested form params with `require` and
lists with `list`.
- How `render`, `redirect`, `head` and `json` build responses, and how to
answer `304 Not Modified` to a conditional `GET`.
- How to use the session, flash messages and cookies.
- How errors become responses, and how to share code between actions.
- How to check permissions with `authorize`, and make sure every action
does.
- How CSRF protection works for forms.
The examples come from the `notes` app introduced in
[Routing](routing.html).
## Controllers and Actions
### Controller Files
A controller lives in `src/controllers/<name>_controller.glacier`. Here is
the one `glacier new` writes:
```elixir
module PagesController do
implements Controller
# Renders src/views/pages/home.html inside src/views/layouts/app.html.
def home do
render title: "Home"
end
end
```
There's no list of controllers to keep: every file in `src/controllers/`
(or any folder) is found when the app builds. `notes_controller.glacier`
(or a plain Rust `notes_controller.rs`) becomes the module
`notes_controller`, and its controller is available as `NotesController`
(and `Notes`), the name the route table uses.
`glacier generate scaffold` writes a controller with all seven resource
actions, and `glacier generate controller Admin dashboard` writes one with
the actions you name, each with a template, a route and a test. See
[Routing](routing.html#verb-routes) for what the latter prints.
### How `.glacier` Becomes Rust
The app's `build.rs` compiles each `.glacier` file into Rust in the hidden,
git-ignored `.glacier/` folder before every build. `module
NotesController do` with `implements Controller` becomes a unit struct
with a `#[glacier::controller]` impl block, and
`create` below becomes:
```rust title=.glacier/controllers/notes_controller.rs
async fn create(ctx: Context) {
let mut note = Note::build(ctx.params());
if note.save().await? {
redirect!(note, notice: "Note created.")
} else {
render!(note; template = new, title = "New note", status = 422)
}
}
```
Each line of the `.glacier` file is the same line of the Rust file, so
compiler errors point at the line you wrote. The `!` after a call awaits
it and passes an error up (`.await?`); a bare `?` passes up an error
without awaiting. The language itself has [a guide of its own](glacier_language.html).
### Actions
`def name do` or `def name(ctx) do` defines an action. The action takes
nothing, or the request's context as `ctx`; anything else is a compile
error telling you to use `defp` (below). Its last expression is the
response, and it returns `glacier::Result`, so it can fail with an error
(see [Errors](#errors)).
```elixir
def show do
note = Note.find(params[:id])!
title = note.title.clone()
render note, title: title
end
```
Any snake_case name can be an action; a route points at it as
`Notes::show`. The seven names `index`, `new`, `create`, `show`, `edit`,
`update` and `destroy` are also what `resources` mounts, and a controller
that defines only some of them gets routes for only those.
Models need no import: every controller sees every model, and the route
helpers (`notes_path()` and the rest).
### Private Helpers with `defp`
`defp` defines a helper that isn't an action. It takes typed arguments
like a Rust function and isn't routable:
```elixir
defp find_note(ctx: &Context) -> Note do
Note.find(ctx.id()?)!
end
```
A `defp` whose body awaits something (uses `!`) becomes an `async fn`
returning `glacier::Result` of its declared type, so calling it with `!`
gives the note or passes the `404` up. Inside the controller, call it on
`Self`:
```elixir
def preview(ctx) do
note = find_note(&ctx)!
render note, layout: :none
end
```
## The Request: `ctx`
An action that declares `(ctx)` gets the request as a `glacier::Context`.
Four parts of it are names of their own in any action, with or without
`(ctx)`: `params`, `session`, `files` and `file("name")`. An action that
uses them gets the request without asking, and an action that binds a
local of the same name (`for session in sessions do`) keeps its own. A
`defp` helper reaches them through the `ctx` it's given:
`ctx.params()`, `ctx.session()`. The parts you'll use most:
| `params[:id]`, `params[:post_id]` | One param as text (`""` if it's missing): `Post.find(params[:id])!` finds by it, and answers `404 Not Found` when it isn't a number |
| `params` | All params as text, from the path, query string and body |
| `ctx.id()` | The `:id` path parameter as an `i64`, for queries such as `Post.where(id: id)`; `404 Not Found` if it's missing or not a number |
| `ctx.param_as::<T>("name")` | A path parameter parsed as `T`; `400 Bad Request` if it doesn't parse |
| `ctx.query::<T>()`, `ctx.json::<T>()`, `ctx.form::<T>()` | The query string, a JSON body or a form body, deserialised into a `serde` type; `400` if they don't fit |
| `file("name")`, `files` | The files uploaded in a `multipart/form-data` body; see [File Uploads](file_uploads.html) |
| `ctx.method()`, `ctx.uri()`, `ctx.path()` | The method, URI and path |
| `ctx.header("name")` | A request header, as `Option<&str>`; `ctx.headers()` gives them all |
| `ctx.remote_ip()` | The client's IP address, as `Option<String>` |
| `session`, `ctx.cookies()` | The session and cookies, below |
| `ctx.flash("notice")`, `ctx.put_flash("notice", "…")` | Read and set flash messages |
| `ctx.state::<T>()` | Shared state registered with `App::state`, such as the `Config` |
`ctx.remote_ip()` is the
connection's address, unless the connection came from a loopback or
private address (a proxy on the same machine or network), in which case
it's the nearest address in `X-Forwarded-For` that isn't one of those.
### Params
`params` collects every parameter as text, as Rails' `params` does:
the query string, then the body (a form, the text fields of a multipart
form, or the top-level fields of a JSON object), then the path parameters,
later ones winning. `get("name")` reads one as an `Option<&str>`:
```elixir
def unlock do
# A stand-in for real sign-in; see the Authentication guide.
if params.get("passcode") != Some("let me in") do
raise Error.forbidden()
end
session.insert("admin", true)?
redirect admin_path(), notice: "Admin area unlocked."
end
def lock do
session.remove("admin")
redirect home_path(), notice: "Admin area locked."
end
```
Models take params whole. `Note.build(params)` and
`note.update(params)` cast the params named after the model's
fields into typed values, and leave other params alone; see
[Models](models.html). A model whose `has_many` is declared `nested` also
takes its children from `update`, as `comments_attributes` for
`has_many :comments, nested`, saving the record and its children together
or not at all; see [Associations](associations.html).
### Nested Params with `require`
A form with fields named `settings[theme]`, or a JSON body like
`{"settings": {"theme": "dark"}}`, nests its params under `settings`.
`require("settings")` returns just those, and is a `400 Bad Request` when
there are none. A file uploaded as `settings[logo]` counts, as it does in
Rails, so a form that sends only a file still has its key:
```elixir
def show(ctx) do
theme = ctx.cookies().get_signed("theme").unwrap_or("light".to_owned())
render theme: theme, title: "Settings"
end
def update(ctx) do
settings = params.require("settings")?
theme = settings.get("theme").unwrap_or("light").to_owned()
ctx.cookies().add_signed(("theme", theme))
redirect settings_path(), notice: "Settings saved."
end
```
```elixir
test "needs the settings params" do
res = patch "/settings", theme: "dark"
assert res.status == :bad_request
assert res.body == "param is missing or the value is empty: settings"
end
```
A JSON body sent to a controller's action
is also read as though it were nested under the controller's name in the
singular, `note` for `Notes`, so `require("note")` finds the fields of a
bare `{"title": "…"}`. Only that one key is wrapped, only for JSON
bodies, and when a `Note` model exists, only its attributes. `Settings` wraps a bare body under `setting`, so its
`require("settings")` still needs the `settings` key:
```elixir
test "wraps a bare JSON body only under the controller's own name" do
res = patch_json "/settings", r#"{"settings": {"theme": "dark"}}"#
assert res.status == :see_other
res = patch_json "/settings", r#"{"theme": "dark"}"#
assert res.status == :bad_request
end
```
Params need no list of allowed fields: a model assigns only its own fields
from params, never its id or timestamps, and never fields marked `protected`.
### Lists with `list`
A form sends a list as the same field repeated with `[]` on its name, such
as a checkbox `<input type="checkbox" name="ids[]" value="7">` for each
note. Each param holds one value, so `get("ids[]")` would give only the
last of them. `list("ids")` gives them all, in order, as an `Array<String>`.
It also reads a JSON array,
`{"ids": [1, 2]}`, and a single `ids=1`, and is empty when there's no such
param. The `notes` app deletes several notes at once with it, from a
`delete "/notes"` route:
```elixir
def destroy_many do
ids: Array<i64> = params.list("ids").filter_map(fn id -> id.parse().ok() end)
deleted = Note.filter(fn note -> note.id.is_in(ids) end).delete_all()!
redirect Note.collection_path(), notice: "Deleted: #{deleted}."
end
```
```elixir
test "deletes every note in ids[]" do
first = insert(Note, title: "First")
second = insert(Note, title: "Second")
third = insert(Note, title: "Third")
res = delete format!("/notes?ids[]={}&ids[]={}", first.id, second.id)
assert res.status == :see_other
assert get(res.location).body =~ "Deleted: 2."
assert Note.count()! == 1
res = delete format!("/notes?ids={}", third.id)
assert get(res.location).body =~ "Deleted: 1."
assert Note.count()! == 0
end
```
A test's form can't repeat a field (setting one again replaces it), so the
test sends the ids in the query string, which `list` reads in the same way.
The values are text, so parse them. `destroy_many` declares
`ids: Array<i64>`, which tells `parse()` what to make, and skips any value
that isn't a number. `delete_all` is covered in
[Querying](querying.html).
### Request Ids (new)
Every request has an id, as in Rails: the `X-Request-Id` a proxy or
load balancer in front sent, if it's a sensible one (letters, digits,
`-`, `_` and `.`, up to 200), or else one made up. It goes back in the
response's `X-Request-Id`, on the request's trace (`glacier.request_id`),
in `Logger`'s JSON lines (`request_id`), and onto the jobs the request
queues, so one id follows the work wherever it goes.
`glacier::request::id()` reads it, to put in an error page or a report:
```glacier
test "every response carries its request id" do
# The one a proxy in front sent, or one made up.
assert get("/").header("x-request-id").is_some()
glacier::testing::header("x-request-id", "abc-123")
assert get("/").header("x-request-id") == Some("abc-123")
end
```
## Responses
### Rendering a Template
`render` renders a template inside the app's layout and answers `200 OK`.
By convention the template is named after the controller and action:
`Notes.show` renders `src/views/notes/show.html`, inside
`src/views/layouts/app.html`.
Arguments become the template's props, the values it declares in its
`{% props %}` line. A variable passes the prop of the same name, and
`name: value` any prop:
```elixir
def index do
page = glacier::pagination::Page.from_params(¶ms, 20)
total = Note.count()!
notes = page.of(Note.order(inserted_at: :desc))!
pinned = case session.get::<i64>("pinned_note_id") do
Some(id) -> Note.find_by(id: id)!
None -> None
end
render notes, pinned: pinned, page: page, total: total, title: "Notes"
end
```
Here `src/views/notes/index.html` declares
`{% props notes: Array<Note>, pinned: Option<Note>, page: glacier::pagination::Page, total: i64 %}`. Leaving a prop out,
or passing one the template doesn't declare, is a compile error. Templates
and layouts are covered in [Views and Templates](views.html).
`render` also takes these options:
| `:name` as the first argument | Renders another template of this controller: `render :new, note` renders `notes/new.html` |
| `title: …` | Sets the layout's `title` prop |
| `in_layout: [head: …, alert: …]` | Sets other props of the layout |
| `status: 422` | Answers with that status instead of `200` |
| `layout: :admin` | Uses `src/views/layouts/admin.html` instead of `app.html` |
| `layout: :none` | Renders the template on its own (so `title:` can't be used with it) |
`create` uses `:new` and `status:` to show the form again with its errors:
```elixir
def create do
note = Note.build(params)
if note.save()! do
redirect note, notice: "Note created."
else
render :new, note, title: "New note", status: 422
end
end
```
WARNING: In `render`, `status:` takes a number, such as `422` (or a
`StatusCode` value). A name like `:unprocessable_entity` doesn't compile
there, though `head` and `json` accept names.
`layout:` picks a different layout, or none:
```elixir
def dashboard do
render count: Note.count()!, title: "Dashboard", layout: :admin
end
```
```elixir
test "previews a note without the layout" do
note = insert(Note, title: "Bare")
res = get preview_note_path(note.id)
assert res.status == :ok
assert res.body =~ "<h2>Bare</h2>"
refute res.body =~ "<!doctype html>"
end
```
NOTE: Keyword arguments go to the template, except `title`, which goes to
the layout. Give the layout other props in `in_layout: […]`. Layout props
that aren't set take their default value (an empty string for a
`String`).
### Redirecting
`redirect` answers `303 See Other`, the right status after a form
submission, to a path or a record:
```elixir
def destroy do
Note.find(params[:id])!.delete()!
redirect Note.collection_path(), notice: "Note deleted."
end
```
`redirect note` goes to the record's own page, `/notes/7`; a path helper
or any string works too, as in `redirect admin_path()`. Keyword arguments
set flash messages for the next request, so `notice: "Note deleted."` is
shown on the page redirected to.
Rails' `redirect_to` sends `302 Found` instead. A browser that gets a
`302` after a `PATCH` or `DELETE` may repeat that method at the new
address, which is why Glacier sends `303`. To send another status, as
when matching a Rails app response for response, give `status:`:
`redirect note, status: :found` answers `302`, and
`status: :moved_permanently` answers `301`.
### `head` and `json`
`head` answers with a status and an empty body. It takes a
status name or a number: `head :no_content` or `head 204`. A response
whose status can have a body, such as `head :ok`, gives the type the request asked for as its `Content-Type`: the type of the
request's format extension (see
[Routing](routing.html#format-extensions)), else the `Accept` header's
preferred type unless it lists `*/*` among other types, as browsers do.
A type Glacier doesn't know, or none, gives `text/html`. A `204` or `304`
has no `Content-Type`. An error status from `head`, such as
`head :forbidden`, stays empty even behind `Pipeline::api()`, whose
`JsonErrors` otherwise gives errors a JSON body.
```elixir
def pin(ctx) do
note = find_note(&ctx)!
session.insert("pinned_note_id", note.id)?
head :no_content
end
```
```elixir
test "pins a note for this session" do
note = insert(Note, title: "Important")
res = post pin_note_path(note.id)
assert res.status == :no_content
assert res.body == ""
refute res.headers.contains_key("content-type")
assert get("/notes").body =~ format!("Pinned: <a href=\"/notes/{}\">Important</a>", note.id)
end
```
`render`, `redirect`, `json` and `head` usually end an action. To answer
early, put `return` in front, as in `unpin`, which only unpins the note
that's pinned:
```elixir
def unpin(ctx) do
note = find_note(&ctx)!
# `return` answers early: only the pinned note can be unpinned.
if session.get::<i64>("pinned_note_id") != Some(note.id) do
return head :conflict
end
session.remove("pinned_note_id")
head :no_content
end
```
```elixir
test "unpins only the pinned note" do
pinned = insert(Note, title: "Important")
other = insert(Note, title: "Other")
post pin_note_path(pinned.id)
res = delete pin_note_path(other.id)
assert res.status == :conflict
res = delete pin_note_path(pinned.id)
assert res.status == :no_content
refute get("/notes").body =~ "Pinned:"
end
```
`json` answers with a value serialised as JSON, with the `Content-Type`
`application/json; charset=utf-8`. A record without a serializer gives its
fields:
```elixir
test "exports a note as JSON" do
note = insert(Note, title: "Data")
res = get note_json_path(note.id)
assert res.status == :ok
assert res.headers["content-type"] == "application/json; charset=utf-8"
assert res.body =~ format!("{{\"id\":{},\"title\":\"Data\"", note.id)
end
```
Serializers, `status:` and the rest of `json` are covered in
[JSON APIs](json_apis.html).
### Conditional GETs with `fresh_when`
A browser or API client that has a page already can ask for it only if it
has changed, by sending back the `ETag` or `Last-Modified` header it was
given, as `If-None-Match` or `If-Modified-Since`. `fresh_when`
answers such a request with an empty `304 Not Modified` when nothing
has changed, so the response body isn't sent again.
`fresh_when(&ctx, last_modified)` is a method on a response, so take the
response that `render` or `json` gives and pass it the time the content
last changed, a `Timestamp` such as a record's `updated_at` (a
`NaiveTimestamp`, such as an `updated_at` stored without a zone, needs `.and_utc()`
first). `render` and `json` give a `glacier::Result`, so `?` takes the
response out of it, and the changed response, the action's last value,
is its result as it is. The
`notes` app's JSON export does this:
```elixir
def export(ctx) do
note = find_note(&ctx)!
res = json note
res?.fresh_when(&ctx, note.updated_at)
end
```
`fresh_when` always adds two headers: `Last-Modified`, the time given, and
a weak `ETag`, `W/"…"`, made from a hash of the body. It then answers
`304 Not Modified`, with no body and no `Content-Type`, when the request's
`If-None-Match` lists that ETag (or is `*`):
```elixir
test "answers 304 Not Modified when the note hasn't changed" do
note = insert(Note, title: "Data")
res = get note_json_path(note.id)
assert res.status == :ok
assert res.headers["last-modified"] == note.updated_at.format("%a, %d %b %Y %H:%M:%S GMT").to_s()
etag = res.headers["etag"].to_str().unwrap().to_owned()
assert etag.starts_with("W/\"")
glacier::testing::header("if-none-match", &etag)
again = get note_json_path(note.id)
assert again.status == :not_modified
assert again.body == ""
end
```
Without an `If-None-Match`, it compares `If-Modified-Since` with the time
given instead, to the second, since HTTP dates have no fractions. A time
that isn't older than the note's `updated_at` gets a `304`; an older one
gets the note:
```elixir
test "compares If-Modified-Since with the note's updated_at" do
note = insert(Note, title: "Data")
since = note.updated_at.format("%a, %d %b %Y %H:%M:%S GMT").to_s()
glacier::testing::header("if-modified-since", &since)
assert get(note_json_path(note.id)).status == :not_modified
glacier::testing::header("if-modified-since", "Wed, 01 Jan 2025 00:00:00 GMT")
assert get(note_json_path(note.id)).status == :ok
end
```
Only a `200 OK` response becomes a `304`. As the ETag is a hash of the
body, the action still does all its work, rendering included; what's
saved is sending the body. Nothing lets an action skip that work when the
response is fresh, and `fresh_when` takes no options for the ETag or
caching: it always sets both headers itself, and sets no `Cache-Control`.
## Sessions, Flash and Cookies
### The Session
The session holds small values for one visitor between requests. It's a
JSON object kept in a cookie, `_glacier_session`, which is encrypted and
signed with the app's `secret_key_base`, so the visitor can neither read
nor change it. Cookies hold about 4 KB, so store ids, not records.
| `session.get::<T>("key")` | The value, as an `Option<T>` |
| `session.insert("key", value)?` | Stores any value `serde` can serialise |
| `session.remove("key")` | Removes one value |
| `session.clear()` | Removes everything, as on signing out |
The `notes` app pins a note for the visitor with `insert` (see `pin`
above), reads it back in `index` with `get::<i64>`, and unlocks its admin
area with `insert("admin", true)`:
```elixir
test "unlocks the admin area for this session" do
insert(Note)
res = post "/admin/session", passcode: "let me in"
assert res.status == :see_other
assert res.location == "/admin"
page = get "/admin"
assert page.status == :ok
assert page.body =~ "Admin area unlocked."
assert page.body =~ "There is 1 note."
assert page.body =~ "<title>Dashboard · notes admin</title>"
end
```
The cookie is only written when the session changes. It's `HttpOnly` and
`SameSite=Lax`, and `Secure` in production.
#### Changing `secret_key_base`
A new `secret_key_base` would sign everyone out, and make every reset or
sign-in link already mailed fail. Keep the old one in
`previous_secret_key_bases` until those have run out:
```toml
secret_key_base = "…" # the new one
previous_secret_key_bases = ["…"] # the old one, for now
```
Cookies, sessions and signed tokens (`glacier::auth::token_for`) made
with a previous secret still read, and a session read with one is
written again with the new secret. After the longest a session lasts
(30 days in the generated authentication), the old secret can go.
### The Flash
A flash message is set during one request and shown during the next,
typically the page a redirect goes to. `redirect … notice: "…"` sets one;
`ctx.put_flash("alert", "…")` sets one directly, as the admin middleware
in [Routing](routing.html#pipelines) does. Any name works; `notice` and
`alert` are the ones the `notes` layout shows. Templates read them with
`flash("notice")`, which returns an `Option<String>`.
A message lasts until the next request that reads the session, which is
normally the page you redirect to:
```elixir
test "shows a flash message once" do
res = post "/notes", params_for(Note)
assert get(res.location).body =~ "<p class=\"flash\">Note created.</p>"
refute get(res.location).body =~ "Note created."
end
```
A flash message is always for the next request: one set during a request
is never shown by that same request.
### Cookies
`ctx.cookies()` reads and sets cookies, in three kinds:
| Plain | `get("name")` | `add(cookie)` | The visitor can read and change it |
| Signed | `get_signed("name")` | `add_signed(cookie)` | Readable, but a changed value is ignored |
| Private | `get_private("name")` | `add_private(cookie)` | Encrypted: neither readable nor changeable |
Each `get` returns an `Option<String>`, and `remove("name")` deletes a
cookie. A cookie can be a `("name", value)` pair, as in the settings
controller above, or a `glacier::cookies::Cookie` for options such as an
expiry. Unless you say otherwise, cookies get `Path=/` and `HttpOnly`.
Signed and private cookies use the same key as the session.
```elixir
test "remembers the theme in a signed cookie" do
res = patch "/settings", settings: [theme: "dark"]
assert res.status == :see_other
assert res.location == "/settings"
page = get "/settings"
assert page.body =~ "Settings saved."
assert page.body =~ "<option value=\"dark\" selected>Dark</option>"
end
```
## Errors (updated)
An action returns `glacier::Result`, and anything that fails with `!` or
`?` ends the action with that error's response:
- A record that `find` can't find is `404 Not Found`, and so is an id
that isn't a number (`Post.find(params[:id])!` on `/posts/abc`).
- `require` with nothing to require, or a body that `ctx.json()` can't
read, is `400 Bad Request`.
- Any other Rust error, such as a database failure, is
`500 Internal Server Error`, and so is a panic in an action.
```elixir
test "answers 404 for a note that doesn't exist" do
res = get "/notes/999"
assert res.status == :not_found
# The app's own 404 page (see Error Pages).
assert res.body =~ "There's no page here"
end
```
To answer with an error yourself, return one of `glacier::Error`'s
constructors: `Error.not_found()`, `Error.forbidden()`,
`Error.bad_request("…")`, `Error.unprocessable("…")`,
`Error.internal("…")`, or `Error.new(status, "message")` for any status.
`raise` leaves the action early with one, as Elixir and Ruby raise:
```elixir
def index do
render slugs: vec!["routing", "views"], title: "Docs"
end
def show(ctx) do
slug = ctx.param("id").unwrap_or_default()
body = case slug do
"routing" -> "<p>Routes live in <code>src/routes.rs</code>.</p>"
"views" -> "<p>Templates live in <code>src/views</code>.</p>"
"controllers-old" -> raise Error.new(StatusCode.GONE, "This page has moved.")
_ -> raise Error.not_found()
end
render body: body, title: slug.to_owned()
end
```
```elixir
test "answers 404 for a doc that doesn't exist" do
assert get("/docs/nope").status == :not_found
end
```
(The docs controller turns the `410` into a redirect, as [Rescuing
Errors](#rescuing-errors) shows.)
A `4xx` error's response is its message as plain text. A `5xx` error's
response is only the status's name, such as `Internal Server Error`; the
message and the chain of underlying errors go to the log instead. In
development, a `5xx` error that isn't JSON (one built from a
`glacier::Error`, including a panic) is replaced by an error page showing the method, path, message and causes. Routes behind
`Pipeline::api()` get JSON errors instead (see [JSON APIs](json_apis.html)).
### Error Pages (new)
An error with no body of its own (a `404` from `find`, a `500`) is
answered, for a browser, with the app's page for its status:
`src/views/errors/404.html`, `500.html` and so on. It's a template like
any other, drawn in the app's layout with the status's name as its
`title`, so it has the flash, helpers and components; it may take
`status: u16`, and nothing else. The `notes` app has one for `404`:
```html
{% props status: u16 %}
<.card title="Not found">
<p>There's no page here ({{ status }}). <a href="{{ notes_path() }}">Back to the notes</a></p>
</.card>
```
```elixir
test "answers a missing page with the app's own, in its layout" do
res = get "/no/such/page"
assert res.status == :not_found
assert res.body =~ "<title>Not Found · notes</title>"
assert res.body =~ "<h2>Not found</h2>"
assert res.body =~ "There's no page here (404)."
end
```
A status without a page falls back to `public/404.html` (a static file,
as Rails' `public/404.html`), then to the status's name. A JSON request
gets `{"status": 404, "error": "Not Found"}`, and in development a `5xx`
still shows the error page above with its causes.
### Rescuing Errors (new)
A controller can answer an error of a given status its own way, as
Rails' `rescue_from` does: `rescue :gone do … end` runs in place of the
error's response when any of its actions fails with `410 Gone`, with the
error as `error`. The docs controller sends a page that has moved to the
list:
```glacier
# A page that has moved goes to the list, with the error's message,
# rather than answering 410.
rescue :gone do
redirect docs_path(), notice: error.message()
end
```
```glacier
test "sends a doc that has moved to the list" do
res = get "/docs/controllers-old"
assert res.status == :see_other
assert res.location == "/docs"
assert get("/docs").body =~ "This page has moved."
# Other errors are answered as before.
assert get("/docs/nope").status == :not_found
end
```
A controller may have a `rescue` for each status it cares about; an
error with another status is answered as above. A `rescue` can do what
an action does (`redirect`, `render`, `json`), or `raise` another error.
It covers the controller's own actions, not a route that doesn't exist
or middleware before them. A status name that doesn't exist fails the
first time a rescued action errs, in development and tests.
A pipeline's middleware can also change error responses on their way
out, as `JsonErrors` does, and `JsonErrors::body` gives an API's errors a
JSON shape of your own (see [JSON APIs](json_apis.html)).
## After the Response (new)
`after_response do … end` runs its body once the response has been
sent, as Laravel's `defer`: work the person needn't wait for, such as
counting a view. It takes copies of what it uses (`id` here, not the
order), runs with the app's database, and logs an error rather than
failing the request:
```glacier
def show do
order = Order.find(params[:id])!
id = order.id
# Counted once the page is sent: the customer doesn't wait for it.
after_response do
glacier::cache::increment(&"orders/#{id}/views", 1, 30.days())!
end
title = "Order #{order.id}"
render order, title: title
end
```
In tests it runs before the response comes back, so a test sees what it
did:
```glacier
test "counts each view of an order after answering" do
order = insert(Order)
get "/orders/#{order.id}"
get "/orders/#{order.id}"
assert glacier::cache::read::<i64>(&"orders/#{order.id}/views")! == Some(2)
end
```
It runs in this process, once, with nothing to retry it: work that must
happen belongs in a [job](background_jobs.html).
## Authorization
`authorize rule` answers `403 Forbidden` unless `rule` is true, and lets
the action carry on if it is. A rule is any expression that gives a
`bool`, often a method on a policy. Policies (`glacier generate policy`),
`VerifyAuthorized`, which reports actions that forget to authorize, and
`skip_authorization` for public actions are covered in [Authentication and
Authorisation](authentication.html#authorisation).
## Middleware for Cross-Cutting Concerns
No hooks run before, after or around an action. Code that should run for
many actions goes in one of two places:
- A `defp` helper the actions call, like `find_note`.
- Middleware, which wraps whole requests. Put it in a pipeline to run it
for a group of routes, or add it to the app's settings in
`src/app.glacier` (`middleware NoIndex`) to run it for every request, including ones that don't match a route
(but not the `/up` health check, which answers before any middleware).
Middleware is a module that `implements Middleware`, one to a file in
`src/middleware/`, found as the app builds. Its one `def`,
`call(ctx, next)`, gets the request and `next`, the rest of the stack. It
can answer by itself, as `RequireAdmin` does when the admin area is
locked, or call `next(ctx)` to pass the request on:
```glacier
# Lets a request through only if this session has unlocked the admin
# area; anyone else goes back to the home page with an alert.
module RequireAdmin do
implements Middleware
def call(ctx, next) do
if session.get::<bool>("admin") == Some(true) do
return next(ctx)
end
redirect home_path(), alert: "Admins only."
end
end
```
`next(ctx)` gives back the response, to return as it is or changed, as
`NoIndex` does:
```glacier
# Asks search engines to leave the admin area out: the page as usual,
# with one more header.
module NoIndex do
implements Middleware
def call(ctx, next) do
response = next(ctx)
response.with_header("x-robots-tag", "noindex")
end
end
```
`session`, `params` and the other request names work as in an action, as
do `!`, `redirect`, `render`, `json` and `head`; an error ends the
request with its response, as an action's does.
In Rust, middleware is a type implementing `glacier::Middleware`, whose
`handle` gets the context and `next` and returns the response
(`next.run(ctx).await` passes the request on).
`glacier::middleware::from_fn` makes middleware from an async closure
instead of a type. Glacier's own middleware includes `Logger`,
`MethodOverride`, `SecureHeaders`, `glacier::csrf::Csrf` and `JsonErrors`,
`glacier::middleware::Throttle`, which limits how often a request can be
made per key, such as per email address, `glacier::middleware::Cors`,
which answers cross-origin requests (see [JSON APIs](json_apis.html)), and
`glacier::middleware::VerifyAuthorized` (see [Authentication and
Authorisation](authentication.html#authorisation)).
## CSRF Protection
The `browser` pipeline checks every `POST`, `PUT`, `PATCH` and `DELETE`
for the session's CSRF token, in a `_csrf_token` form field or an
`X-CSRF-Token` header. Without it, the answer is `403 Forbidden`:
```elixir
test "refuses a form without its CSRF token" do
res = post "/notes", params_for(Note)
assert res.status == :forbidden
assert res.body == "Invalid CSRF token"
end
```
The form helpers `form_for` and `form_tag` include the token, so forms
built with them just work (see [Views and Templates](views.html)). In a
form written by hand, add `{{ csrf_field() }}`. The token is created the
first time a page asks for it and kept in the session.
Controller tests follow the same rules. The test client keeps cookies and
the token from the last form it loaded, as a browser does, which is why
generated tests start by loading a form:
```elixir
# Load a form first, as a browser would, for its CSRF token.
setup do
get "/settings"
end
```
```elixir
test "refuses the wrong passcode" do
res = post "/admin/session", passcode: "guess"
assert res.status == :forbidden
assert res.body == "Forbidden"
end
```
See [Testing](testing.html) for more.
## Trusted Hosts
Every request names the host it's for, in its `Host` header, and whoever
sends the request chooses it. An app that answers for any host can be
reached through someone else's domain (DNS rebinding), and anything that
trusts the header can be fooled. List the hosts the app answers for in
the config:
```toml
# The hosts the app answers for; a request for any other is refused. A
# leading dot allows the domain and its subdomains. The host in `url` is
# allowed too, and outside production, `localhost`.
hosts = [".notes.example.com"]
```
A request for any other host is refused with `403 Forbidden`, before any
middleware runs:
```elixir
test "answers only for its own hosts" do
glacier::testing::header("host", "notes.example.com")
assert get("/").status == :ok
glacier::testing::header("host", "eu.notes.example.com")
assert get("/").status == :ok
glacier::testing::header("host", "evil.example.com")
assert get("/").status == :forbidden
end
```
The host in the `url` setting is always allowed, so in production an app
with `url = "https://example.com"` answers only for `example.com` even
without `hosts`. Outside production the list is checked only when
`hosts` is set, and `localhost` is allowed too, so the development
server and tests work. The health check at `/up` answers whatever the
host, for a load balancer that asks by IP address. In Rust,
`App::new().hosts(["example.com"])` sets the list.