Skip to content

Database::errorPage() returns HTTP 200 — poisons reverse-proxy caches when DB is unreachable #4271

@crandler

Description

@crandler

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

  1. Stop the database server (or set wrong credentials in config/database.php).
  2. Request any URL, e.g. curl -I https://example.com/faq/sitemap.xml.
  3. 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.

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions