star7th/showdoc · critical · RuntimeException

Bootstrap initialization failed: {$e->getMessage()}

Error message

Bootstrap initialization failed: {$e->getMessage()}

What it means

This is the server's composition-root wrapper: bootstrap.php runs on every web request, initializes the database (Database::getInstance(), SQLite by default or MySQL when DB_TYPE=mysql), the cache (CacheManager::getInstance(), optional Redis with errors silently swallowed), and (web SAPI only) Upgrade::checkAndUpgrade() for schema migrations. Any Throwable escaping those three calls is rethrown as RuntimeException('Bootstrap initialization failed: ' . message, 0, $e) for Slim's error handler — so the message you see is the original cause (e.g. a PDO connection error) prefixed with 'Bootstrap initialization failed:'.

Source

Thrown at server/app/Common/bootstrap.php:21

namespace App\Common;

use App\Common\Database\Database;
use App\Common\Database\Upgrade;
use App\Common\Cache\CacheManager;

// 基础引导文件:后续可在此初始化配置、日志等。

try {
    Database::getInstance();
    CacheManager::getInstance();
    
    // 检查并执行数据库升级(仅在 Web 环境下执行,避免 CLI 任务时重复执行)
    if (PHP_SAPI !== 'cli') {
        Upgrade::checkAndUpgrade();
    }
} catch (\Throwable $e) {
    // 初始化失败时抛出异常,让 Slim 错误处理器处理
    throw new \RuntimeException('Bootstrap initialization failed: ' . $e->getMessage(), 0, $e);
}

View on GitHub (pinned to 6a3fa91eee)

Solutions

  1. Read the suffixed original message: 'SQLSTATE[HY000] ... unable to open database file' -> SQLite path/permission issue; 'Connection refused'/'Access denied' -> MySQL config; 'no such table'/'syntax error' near ALTER -> upgrade/schema issue.
  2. SQLite: ensure the Sqlite/ directory and showdoc.db.php exist and are writable by the web user (`mkdir -p Sqlite && chown www-data: Sqlite && chmod 755 Sqlite`), and that pdo_sqlite is enabled (`php -m | grep sqlite`).
  3. MySQL: set DB_TYPE=mysql and verify DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PWD via Env (env file or web-server env), test with `mysql -h $DB_HOST -u $DB_USER -p $DB_NAME`.
  4. Upgrade failures: check the PHP error log for the failing migration statement, back up the database, fix the reported statement's cause (usually permissions or a duplicate column from a re-run), then reload — checkAndUpgrade() only runs on web requests, so one fixed reload completes it.
  5. Temporarily diagnose by running `php -r "require 'server/app/Common/bootstrap.php';"` from CLI (upgrade step is skipped in CLI) to isolate DB init from migration errors.
  6. If open_basedir is set, add the SQLite directory (and Sqlite/showdoc.db.php) to the allowed paths.

Example fix

// before (server/app/Common/bootstrap.php)
try {
    Database::getInstance();
    CacheManager::getInstance();
    if (PHP_SAPI !== 'cli') {
        Upgrade::checkAndUpgrade();
    }
} catch (\Throwable $e) {
    throw new \RuntimeException('Bootstrap initialization failed: ' . $e->getMessage(), 0, $e);
}

// after: fail with an actionable, environment-aware message while keeping the cause chained
} catch (\Throwable $e) {
    $hint = match (true) {
        str_contains($e->getMessage(), 'unable to open database file') => ' Check DB_NAME path exists and is writable by the web user.',
        str_contains($e->getMessage(), 'Connection refused') => ' Check DB_HOST/DB_PORT — the MySQL server is unreachable.',
        default => '',
    };
    throw new \RuntimeException('Bootstrap initialization failed: ' . $e->getMessage() . $hint, 0, $e);
}
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight before any request reaches Slim (e.g. in public/index.php or a health route):
$dbPath = Env::get('DB_NAME', __DIR__ . '/../Sqlite/showdoc.db.php');
if (strtolower(Env::get('DB_TYPE', 'sqlite')) === 'sqlite') {
    $dir = dirname($dbPath);
    if (!is_dir($dir) || !is_writable($dir)) {
        http_response_code(503);
        exit("DB directory not writable: {$dir}");
    }
}

Try / catch

// Slim 4 error middleware / handler: the RuntimeException is already chained ($e->getPrevious()),
// so surface BOTH messages and return 503 instead of a raw 500:
set_error_handler(function (\Throwable $e) {
    $cause = $e->getPrevious() ? ' caused by ' . get_class($e->getPrevious()) . ': ' . $e->getPrevious()->getMessage() : '';
    error_log('[bootstrap] ' . $e->getMessage() . $cause);
});

Prevention

When it happens

Trigger: SQLite mode: DB_NAME path (default Sqlite/showdoc.db.php) missing, outside open_basedir, or not writable by the PHP user; pdo_sqlite extension disabled. MySQL mode (DB_TYPE=mysql): wrong DB_HOST/DB_USER/DB_PWD, server down, unknown database -> PDO connect exception. Upgrade::checkAndUpgrade(): ALTER/CREATE statements failing on a locked or read-only database, or a partially migrated schema from an interrupted upgrade.

Common situations: Fresh clone deployed without creating Sqlite/ directory or without chmod (very common after git pull on a new host); Docker container where the data volume is mounted read-only or owned by root; ops switches DB_TYPE=mysql but leaves default credentials root/''; shared hosting open_basedir restrictions excluding the SQLite path; restoring a backup of an older schema so the first web request's upgrade step fails; Redis being down is NOT a cause here — CacheManager catches its own connection errors.


AI-assisted analysis of star7th/showdoc@6a3fa91eee (2026-08-21). Data as JSON: /api/errors/47561ff3e1d66372. Report an issue: GitHub.