Skip to content

feat(caddy): show DDEV 403 and 404 error pages, fixes #70 - #74

Merged
stasadev merged 1 commit into
mainfrom
20261008_stasadev_error_pages
Oct 8, 2026
Merged

stasadev merged 1 commit into
mainfrom
20261008_stasadev_error_pages

Conversation

@stasadev

@stasadev stasadev commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Short Summary (TL;DR)

Caddy now serves DDEV's 403 and 404 error pages, matching nginx and Apache in DDEV v1.25.4+, instead of an empty response.

The Issue

DDEV v1.25.4 added explanatory 403 and 404 pages for nginx and Apache (ddev/ddev#8590, ddev/ddev#8641). With FrankenPHP, Caddy still returned an empty 403 or 404, for example when docroot is wrong or index.php is missing.

How This PR Solves The Issue

Adds handle_errors 403 404 to the shared Caddyfile snippet. It serves /usr/local/share/ddev-webserver-{403,404}-error.html from the ddev-webserver image with an X-Ddev-40x-Source: ddev-webserver (frankenphp) header.

handle_errors only fires for errors that Caddy produces itself, so 403 and 404 responses from PHP (Laravel, Symfony, etc.) pass through unchanged. A file matcher applies the page and header only when the page file exists. On DDEV older than v1.25.4 it doesn't, so the response stays an empty 403 or 404 as before, without the header.

Also removes a blank line at the top of the Caddyfile that caused a "Caddyfile input is not formatted" warning in ddev logs.

Manual Testing Instructions

mkdir my-laravel-site && cd my-laravel-site
ddev config --project-type=laravel --docroot=public
ddev add-on get ddev/ddev-frankenphp --pr 74
ddev start
ddev composer create-project laravel/laravel
echo "Route::get('/forbidden', fn () => abort(403));" >> routes/web.php

# Laravel's own 404 and 403 pages, without the X-Ddev-*-Source header
curl -sI https://my-laravel-site.ddev.site/missing
curl -sI https://my-laravel-site.ddev.site/forbidden

# Without index.php, the 404 comes from Caddy and shows the DDEV page
mv public/index.php public/index.php.bak
curl -sI https://my-laravel-site.ddev.site/missing
mv public/index.php.bak public/index.php

# On DDEV v1.25.3, the same request returns an empty 404 without the X-Ddev-404-Source header
ddev utility download-ddev --tag v1.25.3
export PATH="$HOME/tmp/ddev-download-ddev/v1.25.3:$PATH"
ddev restart
mv public/index.php public/index.php.bak
curl -sI https://my-laravel-site.ddev.site/missing
mv public/index.php.bak public/index.php

Open the same URLs in a browser to see the pages themselves.

Automated Testing Overview

health_checks requests app-error.php, which returns 404 or 403 from PHP, and checks that the app's body passes through without the DDEV error page. It then temporarily moves index.php away and checks for the 404 page on a missing file and the 403 page on an unreadable file, then restores index.php.

Release/Deployment Notes

Needs DDEV v1.25.4+ for the pages to show.

🤖 Generated with Claude Code

## Short Summary (TL;DR)

Caddy now serves DDEV's 403 and 404 error pages, matching nginx and Apache in DDEV v1.25.4+, instead of an empty response.

## The Issue

- Fixes #70

DDEV v1.25.4 added explanatory 403 and 404 pages for nginx and Apache (ddev/ddev#8590, ddev/ddev#8641). With FrankenPHP, Caddy still returned an empty 403 or 404, for example when `docroot` is wrong or `index.php` is missing.

## How This PR Solves The Issue

Adds `handle_errors 403 404` to the shared Caddyfile snippet. It serves `/usr/local/share/ddev-webserver-{403,404}-error.html` from the ddev-webserver image with an `X-Ddev-40x-Source: ddev-webserver (frankenphp)` header.

`handle_errors` only fires for errors that Caddy produces itself, so 403 and 404 responses from PHP (Laravel, Symfony, etc.) pass through unchanged. A `file` matcher applies the page and header only when the page file exists. On DDEV older than v1.25.4 it doesn't, so the response stays an empty 403 or 404 as before, without the header.

Also removes a blank line at the top of the Caddyfile that caused a "Caddyfile input is not formatted" warning in `ddev logs`.

## Manual Testing Instructions

```bash
mkdir my-laravel-site && cd my-laravel-site
ddev config --project-type=laravel --docroot=public
ddev add-on get ddev/ddev-frankenphp --pr REPLACE_ME_WITH_THIS_PR_NUMBER
ddev start
ddev composer create-project laravel/laravel
echo "Route::get('/forbidden', fn () => abort(403));" >> routes/web.php

# Laravel's own 404 and 403 pages, without the X-Ddev-*-Source header
curl -sI https://my-laravel-site.ddev.site/missing
curl -sI https://my-laravel-site.ddev.site/forbidden

# Without index.php, the 404 comes from Caddy and shows the DDEV page
mv public/index.php public/index.php.bak
curl -sI https://my-laravel-site.ddev.site/missing
mv public/index.php.bak public/index.php

# On DDEV v1.25.3, the same request returns an empty 404 without the X-Ddev-404-Source header
ddev utility download-ddev --tag v1.25.3
export PATH="$HOME/tmp/ddev-download-ddev/v1.25.3:$PATH"
ddev restart
mv public/index.php public/index.php.bak
curl -sI https://my-laravel-site.ddev.site/missing
mv public/index.php.bak public/index.php
```

Open the same URLs in a browser to see the pages themselves.

## Automated Testing Overview

`health_checks` requests `app-error.php`, which returns 404 or 403 from PHP, and checks that the app's body passes through without the DDEV error page. It then temporarily moves `index.php` away and checks for the 404 page on a missing file and the 403 page on an unreadable file, then restores `index.php`.

## Release/Deployment Notes

Needs DDEV v1.25.4+ for the pages to show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stasadev
stasadev force-pushed the 20261008_stasadev_error_pages branch from 563c048 to d67deea Compare October 8, 2026 11:27
@stasadev
stasadev merged commit 4a4572e into main Oct 8, 2026
12 checks passed
@stasadev
stasadev deleted the 20261008_stasadev_error_pages branch October 8, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backport custom 403 and 404 pages in Caddy

1 participant