Skip to content

fix: send HTTP 503 from Database::errorPage()#4272

Merged
thorsten merged 4 commits into
thorsten:4.1from
crandler:fix/database-error-page-http-503
May 16, 2026
Merged

fix: send HTTP 503 from Database::errorPage()#4272
thorsten merged 4 commits into
thorsten:4.1from
crandler:fix/database-error-page-http-503

Conversation

@crandler
Copy link
Copy Markdown
Contributor

@crandler crandler commented May 15, 2026

Closes #4271.

Problem

Database::errorPage() echoes the "Fatal phpMyFAQ Error" body without setting an HTTP status, so the response goes out with the PHP default 200 OK. Downstream caches treat that as a cacheable success and keep serving the error page after the DB has recovered, and status-only health checks (Nagios, uptime probes, k8s readiness) report green during the outage.

Change

Set http_response_code(503) and Retry-After: 60 before emitting the body. Guarded by headers_sent() so any caller that happens to have output something already is not broken with a warning.

503 Service Unavailable is the correct semantic for a transient backend dependency failure; Retry-After is the standard hint for clients and caches.

Test

testErrorPage() extended: resets the status to 200 before the call and asserts that http_response_code() returns 503 after errorPage() runs. The existing string assertions on the body remain unchanged.

Summary by CodeRabbit

  • Bug Fixes

    • Error page now sends a 503 Service Unavailable status and a Retry-After: 60 header when appropriate, improving client-side error handling and retry behavior.
  • Tests

    • Tests updated to verify the HTTP 503 response and Retry-After header are set when the fatal error page is rendered.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d51243a-3195-470f-b676-8c8213038fd5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Database::errorPage() now sets HTTP 503 and a Retry-After: 60 header when headers have not been sent, then renders the fatal error HTML. The test asserts that http_response_code() is 503 after calling errorPage().

Changes

HTTP error status on database failures

Layer / File(s) Summary
Error status headers and validation
phpmyfaq/src/phpMyFAQ/Database.php, tests/phpMyFAQ/DatabaseTest.php
errorPage() checks headers_sent() and, if false, sends HTTP/1.1 503 Service Unavailable and Retry-After: 60 before outputting the error page. Test asserts http_response_code() becomes 503 and headers_list() includes Retry-After: 60 after invoking errorPage().

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 When the DB naps and pages fall still,
I thump a 503 on the mill,
"Retry-After: 60" I softly say,
So caches don't keep the broken display.
Hoppity hop—tests cheer all the way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: send HTTP 503 from Database::errorPage()' directly and clearly summarizes the main change: adding HTTP 503 response code to the errorPage method.
Linked Issues check ✅ Passed The changes implement all coding requirements from issue #4271: setting http_response_code(503), adding Retry-After header, and testing the new behavior with assertions.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing HTTP 503 responses in Database::errorPage() and corresponding test updates, with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/phpMyFAQ/DatabaseTest.php (1)

128-144: ⚡ Quick win

Consider resetting response code in tearDown for test isolation.

The test sets the HTTP response code to 503 but doesn't reset it afterward. While this likely won't affect other tests in this file (none check response codes), it's better practice to clean up global state between tests.

♻️ Suggested approach to reset response code

Add to the tearDown() method (around line 38-49):

protected function tearDown(): void
{
    $this->setStaticPropertyValue('databaseDriver', $this->originalDatabaseDriver);
    $this->setStaticPropertyValue('dbType', $this->originalDbType);
    $this->setStaticPropertyValue('tablePrefix', $this->originalTablePrefix);

    parent::tearDown();
    
    // Reset HTTP response code to default
    http_response_code(200);

    if (isset($this->sqliteTestFile) && is_file($this->sqliteTestFile)) {
        `@unlink`($this->sqliteTestFile);
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpMyFAQ/DatabaseTest.php` around lines 128 - 144, The test
testErrorPage() sets the global HTTP response code to 503 via
http_response_code() but does not restore it, risking test pollution; update the
tearDown() method to reset the global response code (call
http_response_code(200) or the previous value) after parent::tearDown() so tests
are isolated — modify the existing tearDown() that already handles restoring
static properties (databaseDriver, dbType, tablePrefix) to also call
http_response_code(200) and keep any existing cleanup like unlinking
$this->sqliteTestFile.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/phpMyFAQ/DatabaseTest.php`:
- Line 134: Add an assertion to DatabaseTest to verify the Retry-After header
set by Database::errorPage()—after the existing $this->assertEquals(503,
http_response_code()) check, assert that headers_list() (or in_array) contains
the string "Retry-After: 60" (e.g. $this->assertContains('Retry-After: 60',
headers_list()) or $this->assertTrue(in_array('Retry-After: 60',
headers_list()))), so the test validates both the 503 status and the
Retry-After: 60 header.

---

Nitpick comments:
In `@tests/phpMyFAQ/DatabaseTest.php`:
- Around line 128-144: The test testErrorPage() sets the global HTTP response
code to 503 via http_response_code() but does not restore it, risking test
pollution; update the tearDown() method to reset the global response code (call
http_response_code(200) or the previous value) after parent::tearDown() so tests
are isolated — modify the existing tearDown() that already handles restoring
static properties (databaseDriver, dbType, tablePrefix) to also call
http_response_code(200) and keep any existing cleanup like unlinking
$this->sqliteTestFile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bd4cf9c-2f98-4c36-9d67-675dcd0b2d30

📥 Commits

Reviewing files that changed from the base of the PR and between 46b26a5 and 2a740b9.

📒 Files selected for processing (2)
  • phpmyfaq/src/phpMyFAQ/Database.php
  • tests/phpMyFAQ/DatabaseTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • phpmyfaq/src/phpMyFAQ/Database.php

Comment thread tests/phpMyFAQ/DatabaseTest.php
@thorsten
Copy link
Copy Markdown
Owner

@crandler thanks, good catch! Could you please use "4.1" as target branch instead of "main"?

@crandler crandler changed the base branch from main to 4.1 May 15, 2026 14:03
crandler and others added 3 commits May 15, 2026 16:04
When the DB connection fails, errorPage() rendered the error HTML with
the default HTTP 200. That causes downstream caches (nginx proxy_cache,
Varnish, CDN edges) to store the error page and keep serving it after
the DB has recovered, and it makes status-code based health checks blind
to the outage.

Set http_response_code(503) and a Retry-After hint before emitting the
body; guarded with headers_sent() so existing call sites that may have
already produced output are not affected.
http_response_code() emits a 'has no effect' warning when a previous
header('HTTP/...') call has already set the status line, which surfaces
in the test suite with --fail-on-warning. Switch to header() with the
status line plus the response_code parameter -- same effect for real
requests, no warning during tests. Also drop the now-unnecessary status
reset at the top of testErrorPage().
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@crandler crandler force-pushed the fix/database-error-page-http-503 branch from 8004bf4 to 71ad6cb Compare May 15, 2026 14:04
@crandler
Copy link
Copy Markdown
Contributor Author

Heads-up: the <title>Fatal phpMyFAQ Error</title> assertion is duplicated in DatabaseTest.php after the recent test update (lines 70 and 71). Harmless, but probably unintentional. Happy to push a small follow-up commit removing the duplicate if you'd like.

headers_list() is always empty in PHP CLI / PHPUnit runs, so the
Retry-After assertion added in the previous test update could never
match. Use xdebug_get_headers() instead, which records header() calls
even in CLI; guard with function_exists() so the assertion is skipped
gracefully when Xdebug is not loaded.

Also remove the accidentally duplicated <title> assertion.
@thorsten thorsten merged commit 8af13ba into thorsten:4.1 May 16, 2026
3 checks passed
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.

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

2 participants