PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Reader\Exception

The filename $filename is not recognised as an OLE file

Error message

The filename $filename is not recognised as an OLE file

What it means

Thrown by OLERead::read() when the first 8 bytes of the file do not equal the OLE2 signature D0 CF 11 E0 A1 B1 1A E1. OLERead is the lightweight header parser the Xls reader uses to pull streams out of a legacy .xls; a mismatch means the file is not a legacy binary workbook at all (or is damaged), regardless of its extension.

Source

Thrown at src/PhpSpreadsheet/Shared/OLERead.php:81

    /** @var int[] */
    private array $possibleLoop = [];

    /**
     * Read the file.
     */
    public function read(string $filename): void
    {
        File::assertFile($filename);

        // Get the file identifier
        // Don't bother reading the whole file until we know it's a valid OLE file
        $this->data = (string) file_get_contents($filename, false, null, 0, 8);

        // Check OLE identifier
        $identifierOle = pack('CCCCCCCC', 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1);
        if ($this->data != $identifierOle) {
            throw new ReaderException('The filename ' . $filename . ' is not recognised as an OLE file');
        }

        // Get the file data
        $this->data = (string) file_get_contents($filename);

        // Total number of sectors used for the SAT
        $this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);

        // SecID of the first sector of the directory stream
        $this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS);

        // SecID of the first sector of the SSAT (or -2 if not extant)
        $this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS);

        // SecID of the first sector of the MSAT (or -2 if no additional sectors are used)
        $this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS);

        // Total number of sectors used by MSAT

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Use IOFactory::createReaderForFile($path) or IOFactory::load($path) so the reader is chosen by content sniffing, not extension.
  2. If you must pick manually, check the magic bytes first: 'PK...' => Xlsx reader, D0CF11E0... => Xls reader, otherwise treat as CSV/other.
  3. Validate upload size > 0 and content type before parsing; reject early with a clear message.
  4. Ask the sender for a re-export when the signature check fails on a file that should be a real .xls.

Example fix

// before
$spreadsheet = (new \PhpOffice\PhpSpreadsheet\Reader\Xls())
    ->load('export.xls'); // is not recognised as an OLE file (it's really CSV)

// after
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('export.xls');
// or explicit sniffing:
$head = (string) file_get_contents('export.xls', false, null, 0, 4);
$reader = str_starts_with($head, "\xD0\xCF\x11\xE0")
    ? new \PhpOffice\PhpSpreadsheet\Reader\Xls()
    : new \PhpOffice\PhpSpreadsheet\Reader\Csv();
Defensive patterns

Strategy: validation

Validate before calling

$head = (string) @file_get_contents($file, false, null, 0, 4);
if (str_starts_with($head, "\xD0\xCF\x11\xE0")) {
    $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls();
} elseif (str_starts_with($head, 'PK')) {
    $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
} else {
    $reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv();
}

Try / catch

try { $spreadsheet = (new \PhpOffice\PhpSpreadsheet\Reader\Xls())->load($file); }
catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
    if (str_contains($e->getMessage(), 'not recognised as an OLE file')) {
        $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file); // content-based retry
    } else { throw $e; }
}

Prevention

When it happens

Trigger: Reader\Xls::canRead()/load() on a file whose content is not OLE: an .xlsx (ZIP 'PK' header) renamed to .xls, a CSV/HTML export with an .xls extension, or a zero-byte/failed upload. The code reads exactly 8 bytes via file_get_contents(..., 0, 8) and string-compares them to the packed identifier.

Common situations: Hardcoding `new Xls()` because the filename ends in .xls while users upload OOXML or CSV; Excel's 'strict' exports; third-party systems emitting HTML tables named .xls; uploads interrupted so the file is empty.

Related errors


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