readXlsxFile throws on inlineStr cells with no <is> child (openpyxl empty-string cells)
Summary
read-excel-file throws on any .xlsx written by openpyxl that contains an empty-string cell. A single such cell rejects the entire workbook, so no rows are returned at all.
Version tested: 9.3.4 (also present in 9.3.x generally; the inlineStr branch is unchanged since it was added).
Error: Couldn't read "inline string" cell valueRoot cause
openpyxl writes a cell holding an empty string as an inline string with no child element:
<c r="A2" t="inlineStr"><is><t>Marie</t></is></c> <!-- populated -->
<c r="B2" t="inlineStr"></c> <!-- holds "" -->In source/xlsx/parseCell.js, state.inlineString is only initialised when an <is> tag opens:
} else if (tagName === 'is') {
state.is = true;
state.inlineString = '';
}For the second cell no <is> ever opens, so inlineString stays undefined, and parseCellValue.js throws:
case 'inlineStr':
value = inlineString;
if (value === undefined) {
throw new Error('Couldn\'t read "inline string" cell value');
}The throw escapes the sheet parse, so one empty-string cell fails the whole file.
Minimal reproduction
# openpyxl 3.1.5
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.append(["Nom", "Courriel"])
ws.append(["Marie", ""]) # empty string, not None
wb.save("repro.xlsx")import readXlsxFile from 'read-excel-file/node'
await readXlsxFile('repro.xlsx')
// Error: Couldn't read "inline string" cell valueConfirmed to throw on 9.3.4 in both openpyxl's normal and write_only modes. Neither mode emits a sharedStrings.xml, so every string in these files is an inline string.
Only an empty string triggers it
I generated each case and inspected the resulting XML:
| cell value | XML openpyxl writes | breaks |
|---|---|---|
"" |
<c r="B2" t="inlineStr"></c> |
yes |
None |
cell omitted from the row entirely | no |
" " |
<c r="B2" t="inlineStr"><is><t xml:space="preserve"> </t></is></c> |
no |
Real-world impact
This is not a synthetic edge case. openpyxl is the standard Python route to .xlsx, and exporters commonly emit "" rather than None for a missing field.
A concrete example from our codebase: a 200-row contact export written by openpyxl 3.1.5 contains 2211 t="inlineStr" cells, of which 587 have no <is> child. Any one of them alone is fatal, so the file is simply unreadable.
Suggested fix
Treat a missing <is> on an inlineStr cell as an empty string rather than throwing. Either initialise inlineString when the cell opens with t="inlineStr", or default to '' in the inlineStr branch of parseCellValue.
The spec does expect <is> to be present, so openpyxl is arguably at fault. But the reading of a typed cell with no value is unambiguous, other readers (exceljs among them) accept it, and rejecting an entire user-uploaded workbook over it is a harsh outcome. This is the same shape as #68 (closed) (Invalid "shared" string index: undefined), where a single unresolvable string reference also fails the whole file.
If you'd rather keep the strict default, an opt-in such as { tolerateInvalidCells: true } would work just as well for us.
Current workaround
We pre-process the worksheet XML and give bare inlineStr cells an empty <is><t></t></is> before calling readSheet. It works, but it means unzipping and repacking the workbook, which we would like to drop.