glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
---
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:

| Call | Gives |
| --- | --- |
| `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(&params, 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:

| Option | Does |
| --- | --- |
| `: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.

| Call | Does |
| --- | --- |
| `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:

| Kind | Read | Set | |
| --- | --- | --- | --- |
| 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.