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
- Treat as corruption in practice: obtain a clean copy of the workbook and retry before debugging wrapper internals.
- 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.
- Wrap .xls ingestion in try/catch for PhpSpreadsheet\Exception and quarantine the offending file for inspection.
- 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
- Avoid calling Shared\OLE::getStream() directly; use the high-level readers that handle block chains internally.
- Do not hold OLE instances across long lifecycles where the instance registry can change.
- Quarantine .xls files that trigger stream-wrapper failures and inspect them with an OLE viewer.
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
- Only Little-Endian encoding is supported.
- Unsupported PPS type
- Expecting 8 byte string
- Detected loop while iterating blocks
- Parameter pos=$pos is invalid.
AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17).
Data as JSON: /api/errors/16a8bc28a68299c3.
Report an issue: GitHub.