Summary
When phpMyFAQ cannot connect to its database, Database::errorPage() renders a "Fatal phpMyFAQ Error" page but does not set an HTTP error status. The response is sent with the default HTTP/1.1 200 OK, which causes downstream caches and monitoring to treat the error page as a successful response.
Affected code
phpmyfaq/src/phpMyFAQ/Database.php — method Database::errorPage(string $method): void (around line 99 on main).
The method calls echo directly with the HTML body and never invokes http_response_code() or sends a status header.
Impact
- Reverse-proxy cache poisoning: Caches in front of phpMyFAQ (nginx
proxy_cache, Varnish, CDN edge) honour proxy_cache_valid 200, so they store the error page and keep serving it long after the DB has recovered. proxy_cache_use_stale http_5xx cannot rescue it because there is no 5xx.
- Monitoring blind spot: Health checks that only verify the HTTP status code (Nagios
check_http, uptime probes, Kubernetes readiness probes) report "OK" while users actually see an error page.
- SEO: Search engines may index the error page as valid content for affected URLs.
- Misleading semantics: Per RFC 9110,
200 OK indicates the request "has succeeded". A failed DB bootstrap is a server-side error and should be expressed as 503 Service Unavailable.
Reproduction
- Stop the database server (or set wrong credentials in
config/database.php).
- Request any URL, e.g.
curl -I https://example.com/faq/sitemap.xml.
- Observe
HTTP/1.1 200 OK with Content-Type: text/html and the "Fatal phpMyFAQ Error" body.
Suggested fix
Add the two lines below at the top of errorPage():
```php
public static function errorPage(string $method): void
{
http_response_code(503);
header('Retry-After: 60');
echo
'
...
```
503 Service Unavailable is the correct status for a transient backend dependency failure; Retry-After is the conventional hint for clients and caches. The Content-Type is already correct via PHP defaults but could also be set explicitly.
Environment
phpMyFAQ behind nginx reverse-proxy with proxy_cache_valid 200 1h. Cache poisoning observed on production when the upstream MariaDB was briefly unreachable; the bad page was served for over an hour to subsequent users via the cached slot.
Summary
When phpMyFAQ cannot connect to its database,
Database::errorPage()renders a "Fatal phpMyFAQ Error" page but does not set an HTTP error status. The response is sent with the defaultHTTP/1.1 200 OK, which causes downstream caches and monitoring to treat the error page as a successful response.Affected code
phpmyfaq/src/phpMyFAQ/Database.php— methodDatabase::errorPage(string $method): void(around line 99 onmain).The method calls
echodirectly with the HTML body and never invokeshttp_response_code()or sends a status header.Impact
proxy_cache, Varnish, CDN edge) honourproxy_cache_valid 200, so they store the error page and keep serving it long after the DB has recovered.proxy_cache_use_stale http_5xxcannot rescue it because there is no 5xx.check_http, uptime probes, Kubernetes readiness probes) report "OK" while users actually see an error page.200 OKindicates the request "has succeeded". A failed DB bootstrap is a server-side error and should be expressed as503 Service Unavailable.Reproduction
config/database.php).curl -I https://example.com/faq/sitemap.xml.HTTP/1.1 200 OKwithContent-Type: text/htmland the "Fatal phpMyFAQ Error" body.Suggested fix
Add the two lines below at the top of
errorPage():```php
public static function errorPage(string $method): void
{
http_response_code(503);
header('Retry-After: 60');
echo
'
...
```
503 Service Unavailableis the correct status for a transient backend dependency failure;Retry-Afteris the conventional hint for clients and caches. TheContent-Typeis already correct via PHP defaults but could also be set explicitly.Environment
phpMyFAQ behind nginx reverse-proxy with
proxy_cache_valid 200 1h. Cache poisoning observed on production when the upstream MariaDB was briefly unreachable; the bad page was served for over an hour to subsequent users via the cached slot.