---
title: "Error pages"
description: "Brand your app's 404 and 500 pages and take it offline with maintenance mode, all served at the edge without a rebuild."
---

Error pages let you replace dply's default 404 and 500 responses with your own HTML, and turn on maintenance mode, which answers every request with a 503 page until you turn it off. Everything is served from the edge, so changes apply on the next request with no rebuild or redeploy.

## Edit error pages

1. Open your app and choose **Error pages**. Each page (404, 500 and maintenance) is listed with its source: **Custom**, **From dply.yaml** or **Built-in**.
2. Click a page. Paste full HTML, or start from a template, which fills just that page. The 404, blocked-country and maintenance pages offer **Minimal**, **Friendly** and **Enterprise**. The 500 page offers eleven designs: **Default** (the built-in page), **Quiet**, **Request path**, **Auto-retry**, **Receipt**, **Poster**, **Terminal**, **Unplugged**, **Owner hint**, **Signal lost** and **Ghost layout**. A live preview shows the result beside the editor.
3. Choose **Save**. The new page is live on the next request, with no redeploy.

To go back to dply's default, open the page and choose **Use the built-in page**. **Cancel** discards unsaved edits.

Each page can be up to 200,000 characters.

> [!TIP]
> Keep error pages self-contained, with inline CSS and no external scripts or fonts. They're shown when something is wrong, and links back into your app may be what's failing.

## When each page is shown

### 404 page

Shown with status `404` when a request doesn't match a file in your deploy, for static and hybrid apps.

> [!NOTE]
> With SPA fallback on (the default for new apps), a missing path serves your `index.html` with status `200` instead, so your front-end router can handle it, and the 404 page isn't shown. Turn SPA fallback off in **Build** if you want real 404s. For SSR and container apps, your app returns its own 404s.

Without a custom 404 page, dply returns a plain-text `Object not found.`

### Blocked-country page

Shown with status `403` when your [Firewall](/docs/firewall) turns a visitor away by country. Without one, visitors get the plain-text `Forbidden — content is not available in this region (XX).` This page is set in the dashboard only; `dply.yaml` has no key for it.

### 500 page

- **SSR and container apps**: when your app returns a `5xx` HTML response, dply replaces the body with your 500 page (or dply's default error page) and keeps the status code. JSON and other non-HTML error responses pass through unchanged.
- **All apps**: shown if dply's edge itself hits an unexpected error while handling the request. Without a custom 500 page, visitors see dply's default error page. The error details are logged for dply, never shown to visitors.

A 500 page can include these placeholders. dply fills them on every request:

| Placeholder | Filled with |
|---|---|
| `{{dply.ref}}` | A reference for the request (Cloudflare's ray ID) |
| `{{dply.host}}` | The hostname the visitor asked for |
| `{{dply.path}}` | The path the visitor asked for |
| `{{dply.colo}}` | The edge location that served it, such as `ORD` |
| `{{dply.time}}` | The time of the error, in UTC |

#### Seeing your app's own error page

When your app runs with `APP_DEBUG` on (or sends the response header `x-dply-app-debug: 1`), visitors get your app's own error page, stack trace included, instead of the 500 page. Turn it on with **Show full errors (APP_DEBUG)** under **Error pages**, and turn it off before real visitors arrive.

- The 500 page drops any `Content-Security-Policy` header, whether your app sent it or a dply header rule added it, so the page's own styles always work. Your other headers stay.

For a hybrid app's origin, the **Failover HTML** under **Delivery** is shown instead when the origin is down or times out. See [Static & hybrid sites](/docs/static-and-hybrid).

#### Opening a failed request

Click any row under **Live requests** (on the overview or under **Traffic**) to see that request: status, time taken, cache, country and its reference. For a `5xx` from your app, dply keeps the response it returned (headers and body, up to 1 MB) for 7 days, sealed with your error-details password, and shows it there. With `APP_DEBUG` on, that's the full stack trace. For Laravel container apps it also shows the **stack trace**, rebuilt from your app's logs, so you don't need `APP_DEBUG` on. Laravel logs uncaught exceptions to stderr in dply containers, and after a `5xx` dply's PHP image logs the request's reference beside the exception, so the trace shown is that request's own. Apps deployed before this get the exceptions logged closest to the request instead. Other container apps get their error lines from within 30 seconds of the request. **Logs → App output** has the full log.

- Only people who can change the app can see saved responses.
- dply saves at most one response every 5 seconds per path, so a burst of the same error keeps the first.
- After **Make a new password**, older saved responses can no longer be opened.

### Maintenance mode

Turn on **Maintenance mode** to answer every request with your maintenance page (or dply's default "We'll be right back." page):

```http
HTTP/1.1 503 Service Unavailable
Retry-After: 120
Cache-Control: no-store, max-age=0
```

Maintenance mode runs before every other rule, including the [firewall](/docs/firewall) and [access control](/docs/access-control). The checkbox takes effect right away. Turn it off to bring the app back.

> [!NOTE]
> If your organization is paused for billing, dply shows a billing notice in place of your app in the same way. Your own maintenance setting is left unchanged. See [Paused accounts](/docs/paused-accounts).

## Error pages in `dply.yaml`

Declare pages inline or as files in your repository:

```yaml
error_pages:
  html_404_path: public/404.html
  html_500_path: public/500.html

maintenance:
  enabled: false
  html_path: public/maintenance.html
```

Use `html_404`, `html_500` and `maintenance.html` for inline HTML instead of paths. Paths are relative to the repository root and are read at build time.

When both are set:

- For the 404, 500 and maintenance HTML, the dashboard value is used.
- Maintenance mode is on if **either** the dashboard or the repository turns it on. If `maintenance.enabled: true` is in your repository, you have to change the file and redeploy to turn maintenance off.

The **Advanced** section of the **Error pages** page shows what your repository declares.

## Related

- [Routing, redirects & headers](/docs/routing)
- [Troubleshooting errors & 5xx](/docs/guides/troubleshooting-runtime)
- [Waiting room](/docs/waiting-room)
