PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Reader\Exception
Detected loop while iterating blocks
Error message
Detected loop while iterating blocks
What it means
Thrown by OLERead::catchLoop() when a block identifier repeats while walking the OLE block chains (small-block depot / directory streams). Compound documents link blocks in acyclic chains terminated by a special END marker; revisiting a block number proves the chain is cyclic, and parsing stops rather than looping forever.
Source
Thrown at src/PhpSpreadsheet/Shared/OLERead.php:164
$pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE;
$this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs);
$pos += 4 * $bbs;
$sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4);
}
// read the directory stream
$block = $this->rootStartBlock;
$this->entry = $this->readData($block);
$this->readPropertySets();
}
private function catchLoop(int $sbdBlock): void
{
if (in_array($sbdBlock, $this->possibleLoop, true)) {
throw new ReaderException('Detected loop while iterating blocks');
}
$this->possibleLoop[] = $sbdBlock;
}
/**
* Extract binary stream data.
*/
public function getStream(?int $stream): ?string
{
if ($stream === null) {
return null;
}
$streamData = '';
if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) {
/** @var int */
$temp = $this->props[$this->rootentry]['startBlock'];View on GitHub (pinned to 65b080eef4)
Solutions
- Retry from the original source and compare checksums; a cycle almost always means the copy is damaged.
- Open the file in Excel/LibreOffice to confirm the workbook itself is readable; if not, have it re-exported.
- Quarantine uploads that trigger this and alert — cyclic chains in user uploads are a strong corruption/tamper signal.
- Keep PhpSpreadsheet current; loop-detection and malformed-container handling improve across releases.
Example fix
// before
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('upload.xls');
// Detected loop while iterating blocks
// after
try {
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('upload.xls');
} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
if (str_contains($e->getMessage(), 'Detected loop')) {
quarantine('upload.xls'); // keep the bad file for analysis
}
throw new RuntimeException('Damaged workbook — please re-export and re-upload.', 0, $e);
} Defensive patterns
Strategy: try-catch
Try / catch
try { $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file); }
catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
if (str_contains($e->getMessage(), 'Detected loop')) {
quarantine($file);
return ['error' => 'Damaged workbook — please re-export and re-upload.'];
}
throw $e;
} Prevention
- Regard loop detection as a corruption/tamper signal in user uploads, not a transient failure — do not blind-retry the same bytes.
- Keep the library current: loop guards and malformed-FAT handling improve in patch releases.
- Archive offending files; cyclic block chains are worth inspecting if they recur from one source.
When it happens
Trigger: Reading a .xls whose FAT/SAT or directory block chain contains a cycle — corruption, truncation, or a deliberately crafted file. Encountered during Reader\Xls load via OLERead::read()/readData() when following rootStartBlock and dependent chains.
Common situations: Corrupted downloads or partially-written files; hostile uploads built to hang naive parsers (the guard exists precisely for them); storage-level bit rot on archived .xls files.
Related errors
- Only Little-Endian encoding is supported.
- Unable to open stream $path
- Unsupported PPS type
- Expecting 8 byte string
- Parameter pos=$pos is invalid.
AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17).
Data as JSON: /api/errors/b5a2d93278396eeb.
Report an issue: GitHub.