From f2a05fdf68e2066955b9b8146b508b9adc123c37 Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk Date: Wed, 4 Nov 2026 11:10:11 +0200 Subject: [PATCH] fix(caddy): show DDEV 404 page without index.php on Caddy v2.11.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short Summary (TL;DR) Caddy v2.11.8 sends every missing path to `index.php`, even when the docroot has no `index.php`, so FrankenPHP returned a PHP fatal error with status 200. Without `index.php`, the Caddyfile now serves files directly and returns 404 for missing PHP scripts, so the DDEV 403 and 404 pages show again. ## The Issue The scheduled tests for PHP 8.6 fail on all four jobs: https://github.com/ddev/ddev-frankenphp/actions/runs/41799786933 In a project without `index.php`, such as a new project with an empty docroot, every request returns status 200 with this output instead of the DDEV error page: ```text Warning: Unknown: Failed to open stream: No such file or directory in Unknown on line 0 Fatal error: Failed opening required '/var/www/html/index.php' (include_path='.:') in Unknown on line 0 ``` The PHP 8.6.0RC5 FrankenPHP package from https://debs.henderkes.com/ is the first one built with Caddy v2.11.8. That Caddy release changes the `first_exist_fallback` try policy, which `php_server` uses, so that the last entry (`index.php`) always matches, even when the file doesn't exist (GHSA-f278-mgc7-46j3, see https://github.com/caddyserver/caddy/releases/tag/v2.11.8). FrankenPHP doesn't return 404 for a missing script, so PHP fails with status 200. The packages for PHP 8.2 to 8.5 still use Caddy v2.11.7, and they will get the same change when they are rebuilt. ## How This PR Solves The Issue When `index.php` doesn't exist in the docroot, `common_config` handles the request before `php_server`: | Request | Result | | --- | --- | | A path that doesn't end in `.php` | `file_server` serves it, or returns 403 or 404, which show the DDEV error pages | | A `.php` path that doesn't exist | 404 with the DDEV error page | | A `.php` file that exists | `php_server` runs it, as before | Nothing changes when `index.php` exists. The change works with Caddy v2.11.7 and v2.11.8. With Caddy v2.11.8, a docroot without `index.php` returns the DDEV 404 page for `/`. ## Manual Testing Instructions Use PHP 8.6, because only its FrankenPHP package has Caddy v2.11.8 for now: ```bash mkdir -p ~/tmp/frankenphp-no-index && cd ~/tmp/frankenphp-no-index ddev config --project-type=php --php-version=8.6 ddev add-on get ddev/ddev-frankenphp --pr 77 ddev restart ddev exec 'frankenphp version' # Expect Caddy v2.11.8 in the output curl -s -o /dev/null -w '%{http_code}\n' https://frankenphp-no-index.ddev.site/missing.html # Expect 404, and the DDEV 404 page in a browser curl -s -o /dev/null -w '%{http_code}\n' https://frankenphp-no-index.ddev.site/missing.php # Expect 404, and the DDEV 404 page in a browser echo ' index.php curl -s https://frankenphp-no-index.ddev.site/some/route # Expect "ok", because index.php gets all missing paths again ddev delete -Oy frankenphp-no-index ``` Repeat with `--php-version=8.5` to check the same results with Caddy v2.11.7. ## Automated Testing Overview The health checks now also request a missing `.php` file without `index.php`, and expect the DDEV 404 page. Claude Code ran `bats tests --filter-tags php86 --filter-tags php85 --filter-tags php84` on Linux with Docker on amd64, and all 5 tests passed. That covers Caddy v2.11.8 (PHP 8.6), Caddy v2.11.7 (PHP 8.4 and 8.5), worker mode, and a custom docroot. Nobody ran it on arm64 or with DDEV HEAD, which CI covers. 🤖 Developed with assistance from [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 5.5 --- tests/test.bats | 6 ++++++ web-build/Caddyfile.frankenphp | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/test.bats b/tests/test.bats index ebb505f..035a54f 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -211,6 +211,12 @@ health_checks() { assert_output --partial "x-ddev-403-source: ddev-webserver (frankenphp)" assert_output --partial "403: Forbidden" + run curl -s -D - https://${PROJNAME}.ddev.site/missing.php + assert_success + assert_output --partial "HTTP/2 404" + assert_output --partial "x-ddev-404-source: ddev-webserver (frankenphp)" + assert_output --partial "404: Not Found" + run ddev exec 'cd "/var/www/html/${DDEV_DOCROOT}" && mv index.php.bak index.php && rm -f forbidden.txt' assert_success diff --git a/web-build/Caddyfile.frankenphp b/web-build/Caddyfile.frankenphp index 52b5729..47ed453 100644 --- a/web-build/Caddyfile.frankenphp +++ b/web-build/Caddyfile.frankenphp @@ -26,6 +26,22 @@ encode zstd br gzip {$CADDY_SERVER_EXTRA_DIRECTIVES} + # Without index.php, serve files directly, so Caddy's 403 and 404 show the DDEV error pages. + # Caddy v2.11.8+ would send these requests to index.php even when it doesn't exist. + @noIndexFile { + not file index.php + not path *.php *.php/* + } + handle @noIndexFile { + file_server + } + @noIndexMissingScript { + not file index.php + path *.php *.php/* + not file {path} + } + error @noIndexMissingScript 404 + # DDEV error pages (v1.25.4+) for 403/404 from Caddy itself, PHP responses pass through handle_errors 403 404 { @ddevErrorPage file {