Summary
When an xlsx file contains a drawing whose XML root element is not xdr:wsDr (e.g. an unknown or malformed namespace), wb.xlsx.load() hangs indefinitely with no error thrown. The for await loop inside lib/xlsx/xform/base-xform.js never terminates because DrawingXform.parseClose() only returns false (the loop-exit signal) when it sees the literal tag name 'xdr:wsDr' — any other root element keeps returning true forever.
Minimal repro
1. Build the fixture
# Start from any drawing-containing xlsx in the repo
cp spec/integration/data/test-issue-1575.xlsx /tmp/streambuf-repro-base.xlsx
cd /tmp && unzip streambuf-repro-base.xlsx -d streambuf-repro-dir
# Replace the drawing root element
cat > streambuf-repro-dir/xl/drawings/drawing1.xml <<'XML'
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<some:other xmlns:some="http://example.com/unknown-drawing-ns"><some:element/></some:other>
XML
# Repack
cd streambuf-repro-dir && zip -r /tmp/streambuf-repro.xlsx .
2. Load it
const ExcelJS = require('exceljs');
const fs = require('fs');
async function main() {
const wb = new ExcelJS.Workbook();
const buf = fs.readFileSync('/tmp/streambuf-repro.xlsx');
await Promise.race([
wb.xlsx.load(buf),
new Promise((_, rej) => setTimeout(() => rej(new Error('TIMEOUT after 30s')), 30000))
]);
}
main().catch(console.error);
Expected: loads (or throws a parse error)
Actual: hangs indefinitely; the Promise.race timeout never fires because the event loop is blocked
Verified against current upstream master (confirmed by running with gtimeout 12 node repro.js — process killed at 12s with exit code 124).
Root cause
File: lib/xlsx/xform/drawing/drawing-xform.js, parseClose() method (~line 76)
parseClose(name) {
if (this.parser) { ... }
switch (name) {
case this.tag: // this.tag === 'xdr:wsDr'
return false; // ← loop-exit signal
default:
return true; // ← always "keep going" for any other root name
}
}
The parse() loop in lib/xlsx/xform/base-xform.js exits only when parseClose returns false:
// base-xform.js ~line 60
for await (const events of saxParser) {
for (const {eventType, value} of events) {
...
} else if (eventType === 'closetag') {
if (!this.parseClose(value.name)) {
return this.model; // ← only exit path
}
}
}
}
When the root element is anything other than xdr:wsDr, parseClose never returns false, the loop exhausts all XML events, and then blocks waiting for further chunks from the underlying stream — which never come. The hang is total: the event loop is blocked, so even a Promise.race timeout never fires.
Why it matters
Any xlsx file in the wild that has a malformed (but still zip-valid) drawing XML — including files produced by third-party tools that use a different drawing namespace — will silently hang the load path with no error, no rejection, and no timeout, making it impossible to handle in application code.
Suggested fix
In DrawingXform.parseClose(), the default branch (or the end of the loop after all events are consumed) should return false (or throw) when the root element was never recognized as xdr:wsDr, rather than looping indefinitely.
Summary
When an xlsx file contains a drawing whose XML root element is not
xdr:wsDr(e.g. an unknown or malformed namespace),wb.xlsx.load()hangs indefinitely with no error thrown. Thefor awaitloop insidelib/xlsx/xform/base-xform.jsnever terminates becauseDrawingXform.parseClose()only returnsfalse(the loop-exit signal) when it sees the literal tag name'xdr:wsDr'— any other root element keeps returningtrueforever.Minimal repro
1. Build the fixture
2. Load it
Expected: loads (or throws a parse error)
Actual: hangs indefinitely; the
Promise.racetimeout never fires because the event loop is blockedVerified against current upstream master (confirmed by running with
gtimeout 12 node repro.js— process killed at 12s with exit code 124).Root cause
File:
lib/xlsx/xform/drawing/drawing-xform.js,parseClose()method (~line 76)The
parse()loop inlib/xlsx/xform/base-xform.jsexits only whenparseClosereturnsfalse:When the root element is anything other than
xdr:wsDr,parseClosenever returnsfalse, the loop exhausts all XML events, and then blocks waiting for further chunks from the underlying stream — which never come. The hang is total: the event loop is blocked, so even aPromise.racetimeout never fires.Why it matters
Any xlsx file in the wild that has a malformed (but still zip-valid) drawing XML — including files produced by third-party tools that use a different drawing namespace — will silently hang the load path with no error, no rejection, and no timeout, making it impossible to handle in application code.
Suggested fix
In
DrawingXform.parseClose(), thedefaultbranch (or the end of the loop after all events are consumed) should returnfalse(or throw) when the root element was never recognized asxdr:wsDr, rather than looping indefinitely.