PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Exception

Unable to open stream $path

Error message

Unable to open stream $path

What it means

Thrown by Shared\OLE::getStream() when fopen() cannot open the custom 'ole-chainedblockstream://' wrapper URL it builds from an OLE instance id, block id and size. The wrapper is the mechanism that exposes a chain of FAT blocks inside the compound document as a readable PHP stream; failure means the wrapper rejected those parameters or the wrapper was not registered.

Source

Thrown at src/PhpSpreadsheet/Shared/OLE.php:236

            $isRegistered = true;
        }

        // Store current instance in global array, so that it can be accessed
        // in OLE_ChainedBlockStream::stream_open().
        // Object is removed from self::$instances in OLE_Stream::close().
        $GLOBALS['_OLE_INSTANCES'][] = $this; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I don't know how to fix this)
        $keys = array_keys($GLOBALS['_OLE_INSTANCES']); //* @phpstan-ignore argument.type (I don't know how to fix this)
        $instanceId = end($keys);

        $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId;
        if ($blockIdOrPps instanceof OLE\PPS) {
            $path .= '&blockId=' . $blockIdOrPps->startBlock;
            $path .= '&size=' . $blockIdOrPps->Size;
        } else {
            $path .= '&blockId=' . $blockIdOrPps;
        }

        $resource = fopen($path, 'rb') ?: throw new Exception("Unable to open stream $path");

        return $resource;
    }

    /**
     * Reads a signed char.
     *
     * @param resource $fileHandle file handle
     */
    private static function readInt1($fileHandle): int
    {
        [, $tmp] = unpack('c', fread($fileHandle, 1) ?: '') ?: [0, 0];
        /** @var int $tmp */

        return $tmp;
    }

    /**

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Treat as corruption in practice: obtain a clean copy of the workbook and retry before debugging wrapper internals.
  2. Let the high-level readers do the work: Reader\Xls (via OLERead) handles block streams for you; avoid calling Shared\OLE::getStream() directly unless you maintain custom OLE tooling.
  3. Wrap .xls ingestion in try/catch for PhpSpreadsheet\Exception and quarantine the offending file for inspection.
  4. If it recurs on one specific file, inspect it with an OLE viewer (e.g. Structured Storage eXplorer) to confirm broken block chains.

Example fix

// before
$resource = $ole->getStream($pps); // Unable to open stream ole-chainedblockstream://oleInstanceId=3&blockId=...

// after
try {
    $resource = $ole->getStream($pps);
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
    // malformed block chain in the container — reject the file
    throw new RuntimeException('Workbook container is corrupt: ' . $e->getMessage(), 0, $e);
}
Defensive patterns

Strategy: try-catch

Try / catch

try { $resource = $ole->getStream($pps); }
catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
    if (str_contains($e->getMessage(), 'Unable to open stream')) {
        throw new RuntimeException('Workbook block chain is corrupt: ' . $e->getMessage(), 0, $e);
    }
    throw $e;
}

Prevention

When it happens

Trigger: Calling OLE::getStream($blockIdOrPps) where the OLE instance index in $GLOBALS['_OLE_INSTANCES'] no longer matches (instance registry mutated), or the blockId/size are invalid for the file; the ChainedBlockStream wrapper's stream_open() returns false, e.g. when the referenced OLE object is gone or the block lies outside the parsed FAT. Most realistic with corrupted .xls files or misuse of the internal OLE API.

Common situations: Corrupted or hostile .xls uploads whose allocation tables point at nonexistent blocks; code that kept a stale OLE object and calls getStream() after the instance registry changed; edge cases in heavily fragmented workbooks with malformed block chains.

Related errors


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