PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Exception

Failed to load file {filename} as a DOM Document

Error message

Failed to load file {filename} as a DOM Document

What it means

In Html::loadIntoExisting(), the file content is first passed through the security scanner (XXE/external-entity protection), optionally transformed, then handed to DOMDocument::loadHTML(). If loadHTML returns false, or anything throwable happens along the way (including the scanner rejecting the content), the reader throws this exception with the original failure chained as getPrevious(). libxml diagnostics are captured in the reader's libxml messages for inspection.

Source

Thrown at src/PhpSpreadsheet/Reader/Html.php:823

            $useErrors = libxml_use_internal_errors($this->suppressLoadWarnings);
        } else {
            $useErrors = null;
        }

        try {
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
            $convert = static::replaceNonAsciiIfNeeded($convert);
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
        } catch (Throwable $e) {
            $loaded = false;
        } finally {
            $this->libxmlMessages = libxml_get_errors();
            if (is_bool($useErrors)) {
                libxml_use_internal_errors($useErrors);
            }
        }
        if ($loaded === false) {
            throw new Exception('Failed to load file ' . $filename . ' as a DOM Document', 0, $e ?? null);
        }
        self::loadProperties($dom, $spreadsheet);

        return $this->loadDocument($dom, $spreadsheet);
    }

    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
    {
        $properties = $spreadsheet->getProperties();
        foreach ($dom->getElementsByTagName('meta') as $meta) {
            $metaContent = (string) $meta->getAttribute('content');
            if ($metaContent !== '') {
                $metaName = (string) $meta->getAttribute('name');
                switch ($metaName) {
                    case 'author':
                        $properties->setCreator($metaContent);

                        break;

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Inspect exception->getPrevious() and the reader's captured libxml messages to find the true cause
  2. If the scanner rejected it, strip risky constructs (DOCTYPE, ENTITY declarations) before loading
  3. Repair the markup with ext-tidy (or a sanitizer) and retry; fall back to loadSpreadsheetFromString on cleaned content

Example fix

// before
$spreadsheet = (new Html())->load($uploadPath); // scanner rejects DTD -> 'Failed to load file ... as a DOM Document'

// after
$clean = preg_replace('/<!DOCTYPE[^>]*>|<!ENTITY[^>]*>/i', '', (string) file_get_contents($uploadPath));
$reader = new Html();
try {
    $spreadsheet = $reader->loadSpreadsheetFromString($clean);
} catch (Exception $e) {
    error_log('HTML load failed: ' . (string) $e->getPrevious());
    throw $e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

$source = (string) file_get_contents($filename);
if (preg_match('/<!DOCTYPE|<!ENTITY/i', $source) === 1) {
    $filename = null; // flag: sanitize before loading
    $source = preg_replace('/<!DOCTYPE[^>]*>|<!ENTITY[^>]*>/i', '', $source);
    // load the sanitized string via loadSpreadsheetFromString instead of the file
}

Try / catch

try {
    $spreadsheet = (new Html())->load($filename);
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
    $cause = $e->getPrevious();
    if ($cause instanceof SecurityException || str_contains((string) $cause, 'Entity')) {
        // security scanner rejected the markup — sanitize and retry once
    }
    throw $e;
}

Prevention

When it happens

Trigger: Grossly malformed or binary content fed as HTML; content rejected by the security scanner (DOCTYPE/external-entity patterns); scanFile/transform steps returning null; encoding breakage that makes libxml bail.

Common situations: Parsing untrusted or scraped HTML of unknown quality; files containing DTD/ENTITY declarations; upstream content silently changing format (JSON error pages saved as .html).

Related errors


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/a8566677335a0761. Report an issue: GitHub.