---
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
controller Pages do
# Renders src/views/pages/home.html inside src/views/layouts/app.html.
def home do
render title: "Home"
end
end
```
Every controller is listed in `src/controllers/mod.rs`, which the
generators keep up to date:
```rust
// Declares each controller, from src/controllers/<name>_controller.glacier
// (compiled by build.rs into the hidden .glacier/ folder) or a plain
// <name>_controller.rs.
glacier::controllers!(pages, notes, admin, settings, docs, attachments, sections, downloads);
```
`glacier::controllers!(pages, notes, …)` finds
`src/controllers/notes_controller.glacier` (or a plain Rust
`notes_controller.rs`), declares it as the module `notes_controller`, and
makes its controller available as `Notes`, which is 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. `controller Notes do`
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(ctx) do
note = Note.find(ctx.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 = Self.find_note(&ctx)!
render note, layout: :none
end
```
## The Request: `ctx`
`ctx` is a `glacier::Context`. The parts you'll use most:
| `ctx.id()` | The `:id` path parameter as an `i64`; `404 Not Found` if it's missing or not a number |
| `ctx.param("name")` | A path parameter, as `Option<&str>` |
| `ctx.param_as::<T>("name")` | A path parameter parsed as `T`; `400 Bad Request` if it doesn't parse |
| `ctx.params()` | All params as text, from the path, query string and body |
| `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 |
| `ctx.file("name")`, `ctx.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>` |
| `ctx.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
`ctx.params()` collects every parameter as text:
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(ctx) do
# A stand-in for real sign-in; see the Authentication guide.
if ctx.params().get("passcode") != Some("let me in") do
return Err(Error.forbidden())
end
ctx.session().insert("admin", true)?
redirect admin_path(), notice: "Admin area unlocked."
end
def lock(ctx) do
ctx.session().remove("admin")
redirect home_path(), notice: "Admin area locked."
end
```
Models take params whole. `Note.build(ctx.params())` and
`note.update(ctx.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:
```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 = ctx.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, and only for JSON
bodies. `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 a `Vec<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(ctx) do
ids: Vec<i64> = ctx.params().list("ids").iter().filter_map(fn id -> id.parse().ok() end).collect()
deleted = Note.filter(fn note -> note.id.is_in(ids) end).delete_all()!
redirect Note.collection_path(), notice: format!("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: Vec<i64>`, which tells `parse()` what to make, and skips any value
that isn't a number. `delete_all` is covered in
[Querying](querying.html).
## 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(ctx) do
notes = Note.order(inserted_at: :desc)!
pinned_id = ctx.session().get::<i64>("pinned_note_id")
pinned = notes.iter().find(fn note -> Some(note.id) == pinned_id end).cloned()
render notes, pinned: pinned, title: "Notes"
end
```
Here `src/views/notes/index.html` declares
`{% props notes: Vec<Note>, pinned: Option<Note> %}`. 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 |
| `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(ctx) do
note = Note.build(ctx.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: `title` is the only layout prop `render` sets from `.glacier`. Other
keyword arguments go to the template, and 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(ctx) do
Note.find(ctx.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.
### `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 = Self.find_note(&ctx)!
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 = Self.find_note(&ctx)!
# `return` answers early: only the pinned note can be unpinned.
if ctx.session().get::<i64>("pinned_note_id") != Some(note.id) do
return head :conflict
end
ctx.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 `Ok(…)` makes the action's result again. The
`notes` app's JSON export does this:
```elixir
def export(ctx) do
note = Self.find_note(&ctx)!
res = json note
Ok(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_string()
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_string()
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.
| `ctx.session().get::<T>("key")` | The value, as an `Option<T>` |
| `ctx.session().insert("key", value)?` | Stores any value `serde` can serialise |
| `ctx.session().remove("key")` | Removes one value |
| `ctx.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.
### 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 selected>dark</option>"
end
```
## Errors
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 `ctx.id()`
on a path whose `:id` isn't a number.
- `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
assert res.body == "Not Found"
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.
`return Err(…)` leaves the action early:
```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" -> return Err(Error.new(StatusCode.GONE, "This page has moved."))
_ -> return Err(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
test "answers 410 for a doc that has moved" do
res = get "/docs/controllers-old"
assert res.status == :gone
assert res.body == "This page has moved."
end
```
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)).
NOTE: There's no controller-level error handler, and no custom HTML
error pages, yet. A
pipeline's middleware can 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)).
## 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 with `App::middleware` in `src/lib.rs`
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 Rust: a type implementing `glacier::Middleware`, whose
`handle` gets the context and `next`, the rest of the stack. It can answer
by itself, as `RequireAdmin` does when the admin area is locked, or call
`next.run(ctx).await` and return (or change) the response:
```rust
/// Lets a request through only if this session has unlocked the admin
/// area; anyone else goes back to the home page with an alert.
pub struct RequireAdmin;
impl Middleware for RequireAdmin {
fn handle(&self, ctx: Context, next: Next) -> BoxFuture<Response> {
Box::pin(async move {
if ctx.session().get::<bool>("admin") == Some(true) {
return next.run(ctx).await;
}
ctx.put_flash("alert", "Admins only.");
Response::redirect(&crate::routes::home_path())
})
}
}
```
`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.