|
| 1 | +# Error page resolution |
| 2 | + |
| 3 | +The runner can serve different error pages per-project (and per group of projects), instead of |
| 4 | +falling back to one generic error page for every form hosted by the runner. |
| 5 | + |
| 6 | +## Throwing an application error |
| 7 | + |
| 8 | +Two error classes are available from [`errors.ts`](../../runner/src/server/plugins/engine/errors.ts) |
| 9 | +for code that needs to control how an error is displayed: |
| 10 | + |
| 11 | +- `RenderingError`: thrown by the engine itself (e.g. no form or page found for a given id/path). |
| 12 | + Takes a `code` (HTTP status code). |
| 13 | +- `ControllerError`: thrown from page controllers when a server-side action fails (e.g. a lookup |
| 14 | + in a page's `onGet`/`onPost` handler). Takes a `code`, and an optional `page` which names a |
| 15 | + specific view to render instead of the generic status-code view. |
| 16 | + |
| 17 | +```ts |
| 18 | +import { ControllerError } from "server/plugins/engine/errors"; |
| 19 | + |
| 20 | +throw new ControllerError("Failed to find address", { |
| 21 | + code: 500, |
| 22 | + page: "example-error", |
| 23 | +}); |
| 24 | +``` |
| 25 | + |
| 26 | +Both classes are Boom errors under the hood, so throwing one still results in a Boom response |
| 27 | +that hapi's `onPreResponse` extension can intercept. |
| 28 | + |
| 29 | +## Resolving the view |
| 30 | + |
| 31 | +The resolution logic lives in [`errorPages.ts`](../../runner/src/server/plugins/errorPages.ts), |
| 32 | +registered as the `error-pages` plugin's `onPreResponse` handler. For every Boom response it: |
| 33 | + |
| 34 | +1. Extracts the **form id** from the first segment of the request path |
| 35 | + (`extractFormIdFromPath`), e.g. `/my-form/page-one` → `my-form`. |
| 36 | +2. Looks up `server.app.forms[formId]` to get the form's model, and reads its **group** from |
| 37 | + `form.def.formGroup` (the optional `formGroup` field on the form JSON). |
| 38 | +3. If the thrown error's message identifies it as a `ControllerError` or `RenderingError`, it is |
| 39 | + treated as an **application error** and handled by `handleApplicationError`, which resolves a |
| 40 | + view by checking, in order, most-to-least specific: |
| 41 | + - `page` view (from `ControllerErrorMetadata.page`, if provided) in the **form** folder, |
| 42 | + then the **group** folder, then the generic `views/` folder. |
| 43 | + - if no `page` view exists (or none was set on the error), the same lookup is repeated using |
| 44 | + the numeric status **code** (e.g. `404`) as the view name. |
| 45 | + - if none of those exist, the status code itself is used as the view name, which resolves to |
| 46 | + the existing top-level view (e.g. `views/404.html`). |
| 47 | +4. Any other Boom error falls back to the pre-existing behaviour: |
| 48 | + - a `403` renders `csrf-protection` (CSRF token failures), passing the form id and form |
| 49 | + name into the view. |
| 50 | + - anything else renders the generic `500` view, passing the form name (or the configured |
| 51 | + `serviceName`) into the view. |
| 52 | +5. If a form cannot be resolved for the form id (e.g. the path doesn't map to a known form), |
| 53 | + resolution falls back to the plain `500` view with no form context. |
| 54 | + |
| 55 | +## View lookup order |
| 56 | + |
| 57 | +Given a form id, an optional group, and a view name (either a `page` name or a status code), |
| 58 | +`findView` checks folders under `runner/src/server/views/` in this order and returns the first |
| 59 | +one that exists on disk: |
| 60 | + |
| 61 | +``` |
| 62 | +views/<PROJECT_ID>/<name>.html |
| 63 | +views/<GROUP>/<name>.html |
| 64 | +views/<name>.html |
| 65 | +``` |
| 66 | + |
| 67 | +For example, a `RenderingError` with `code: 404` thrown while serving a form with id |
| 68 | +`my-form` and form group `my-form-group` will look for: |
| 69 | + |
| 70 | +``` |
| 71 | +views/my-form/404.html |
| 72 | +views/my-form-group/404.html |
| 73 | +views/404.html |
| 74 | +``` |
| 75 | + |
| 76 | +A `ControllerError` with `code: 500, page: "example-error"` thrown in the same context will |
| 77 | +first look for: |
| 78 | + |
| 79 | +``` |
| 80 | +views/my-form/example-error.html |
| 81 | +views/my-form-group/example-error.html |
| 82 | +views/example-error.html |
| 83 | +``` |
| 84 | + |
| 85 | +and only fall back to the `500` lookup above if none of the `example-error.html` files exist. |
| 86 | + |
| 87 | +## Adding a form or form group error page |
| 88 | + |
| 89 | +Add an `.html` view under a folder matching the `formId` or `formGroup` value, using the same name |
| 90 | +(`<code>.html` or a custom page name) as an existing generic view. No registration is required; |
| 91 | +`findView` checks for the file's existence on disk at error time via `fs.existsSync`. |
| 92 | + |
| 93 | +``` |
| 94 | +runner/src/server/views/ |
| 95 | +├── 404.html # generic fallback |
| 96 | +├── my-form-group/ |
| 97 | +│ └── 404.html # shared by all forms with form group: "my-form-group" |
| 98 | +└── my-form/ |
| 99 | + └── 404.html # used only by the "my-form" project |
| 100 | +``` |
0 commit comments