BuildNot yet confirmed elsewhere1 publisher3 min readPublished
A parser you can read beats one you trust: hand-rolling .xlsx to keep a CSP promise
A bank-statement converter enforcing connect-src 'none' skipped SheetJS and implemented the four files it needed. Then a green 26-test suite missed a CSP failure only the real page could show.
The Engineer · Build desk
What happened
- The author runs a small site that converts bank CSV exports into the file format QuickBooks Desktop accepts, and the whole thing runs client-side.
- Every page of the site ships a Content Security Policy with connect-src 'none', so the browser refuses to let the page make any network request at all.
- With the Network tab open during a conversion, it stays empty; the author describes this as the feature rather than an add-on.
- When adding Excel support, the obvious choice was SheetJS; the author decided against it, and says the reason was not bundle size.
- The author's argument: the claim "your file never leaves the browser, and here is the policy that enforces it" becomes "and also trust this dependency" once a large third-party parser is included, which he calls a materially weaker claim for a tool that handles people's bank statements.
Why it matters
A developer who runs a client-side converter for bank statements added .xlsx support and declined to install SheetJS, on the grounds that a large third-party parser weakens the specific privacy claim the site makes [5][6]. What makes the decision worth reading is the accounting behind it: he enumerated the subset of the format he actually needed, and it was small [7][8].
The site turns bank CSV exports into the file format QuickBooks Desktop accepts, and the guarantee is enforced rather than asserted [2]. Every page ships a Content Security Policy with connect-src 'none', so the browser will not permit the page to make any network request at all [3]. Open the Network tab during a conversion and it stays empty [4]. His objection to the dependency was not bundle size [5]. It was that "your file never leaves the browser, and here is the policy that enforces it" degrades into "and also trust this dependency", which he calls a materially weaker claim for a tool handling people's bank statements [6].
Then the inventory. An .xlsx is a ZIP of XML: xl/workbook.xml lists the sheets, xl/worksheets/sheet1.xml holds the cells, xl/sharedStrings.xml is a deduplicated string pool that cells reference by index, and xl/styles.xml carries the number formats [7]. Reading that needs three capabilities, two of which the browser already provides [8]. Unzipping means walking the ZIP central directory, about forty lines [9]. Decompression is native, via DecompressionStream('deflate-raw') [10]. For the XML he hand-rolled a tag scanner instead of using DOMParser, because his tests run in Node, where DOMParser does not exist, and he wanted one code path rather than two [11]. Net result: two of the three pieces written by hand, one of them replacing something the platform supplies [23].
Dates cost the most care, because Excel does not store dates. A cell holding 5 January 2024 contains the number 45296, and whether that renders as a date depends on the cell's number format, which lives in a different file inside the archive [12]. So the parser reads styles.xml, works out which style indexes correspond to date formats, and checks every numeric cell against that list [13]. Neither failure mode raises an error [14]. Missing a date format leaves a column of five-digit numbers, which at least looks wrong; treating a currency format as a date turns an amount of 45296 into a day in January 2024, which does not [15]. The epoch is 1899-12-30 rather than 1900-01-01, because Excel treats 1900 as a leap year and reserves serial 60 for a 29 February that never existed [16]. Older Mac files use a 1904 epoch flagged by date1904="1" on workbookPr, and missing that attribute puts every date off by four years and a day [17].
The instructive part is the bug. Twenty-six tests against workbooks the suite generates itself, covering date handling, sparse cells, shared strings and sheet selection, all passing [18]. He then dropped a file on the real page and got back one line: Failed to fetch [19]. The cause was collecting the decompression output by wrapping the stream in a Response and calling arrayBuffer(), which Chrome counts as a fetch, which connect-src 'none' blocks, producing what reads like a network error in code with no network [1]. Draining the reader by hand avoids it [20].
Worth watching in your own build: whether the environment your tests run in enforces the constraint your product sells. Node does not enforce CSP, so the green suite could never have caught this [21]. A smaller companion detail is corrupt input, where the writer side of the stream rejects alongside the reader, so without a .catch() on both writer.write() and writer.close() an Uncaught (in promise) lands next to the real error [22].
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence46
- Adoption14
- Hype gap−8
- Incentives62
- Confidence54
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The offending code wrapped the decompression stream in a Response and called arrayBuffer(); Chrome treats that as a fetch, connect-src 'none' blocks it, and the resulting error reads like a network failure in code that has no network.
- [2]
The author runs a small site that converts bank CSV exports into the file format QuickBooks Desktop accepts, and the whole thing runs client-side.
- [3]
Every page of the site ships a Content Security Policy with connect-src 'none', so the browser refuses to let the page make any network request at all.
- [4]
With the Network tab open during a conversion, it stays empty; the author describes this as the feature rather than an add-on.
- [5]
When adding Excel support, the obvious choice was SheetJS; the author decided against it, and says the reason was not bundle size.
- [6]
The author's argument: the claim "your file never leaves the browser, and here is the policy that enforces it" becomes "and also trust this dependency" once a large third-party parser is included, which he calls a materially weaker claim for a tool that handles people's bank statements.
- [7]
An .xlsx file is a ZIP archive containing XML: xl/workbook.xml lists the sheets, xl/worksheets/sheet1.xml holds the cells, xl/sharedStrings.xml is a deduplicated string pool that cells reference by index, and xl/styles.xml carries the number formats.
- [8]
Reading that subset needs three capabilities, two of which the browser already provides.
- [9]
Unzipping means walking the ZIP central directory, which is about forty lines of code.
- [10]
Decompression is handled by the native DecompressionStream('deflate-raw').
- [11]
The author hand-rolled a tag scanner for the XML rather than using DOMParser, because the tests run in Node where DOMParser does not exist, and he preferred one code path to two.
- [12]
Excel does not store dates as dates: a cell holding 5 January 2024 contains the number 45296, and whether it should display as a date depends on the cell's number format, which lives in a different file inside the archive.
- [13]
The parser must read styles.xml, work out which style indexes correspond to date formats, and check every numeric cell against that list.
- [14]
Both date-handling failure modes are bad, and neither of them raises an error.
- [15]
Missing a date format makes the user's date column arrive as five-digit numbers, which at least looks wrong; treating a currency format as a date makes an amount of 45296 silently become a day in January 2024, which does not.
- [16]
The serial epoch is 1899-12-30 rather than 1900-01-01, because Excel believes 1900 was a leap year and reserves serial 60 for a 29 February that never existed; starting a day and a half earlier aligns everything from March 1900 onward.
- [17]
Older Mac files use a 1904 epoch, flagged by date1904="1" on workbookPr, and missing that attribute puts every date in the file off by four years and a day.
- [18]
The author wrote 26 tests against workbooks the test suite generates itself, covering date handling, sparse cells, shared strings and sheet selection, and all of them passed.
- [19]
Opening the real page in a browser and dropping a file on it returned a single line: Failed to fetch.
- [20]
Draining the stream by hand with readable.getReader() and a read loop avoids the problem entirely.
- [21]
Because Node does not enforce CSP, the test suite could never have caught the failure; the author's conclusion is that a green suite told him nothing about whether the code worked on the page it was written for.
- [22]
When the compressed data is corrupt, the writer side of the stream rejects as well as the reader side, so without a .catch() on both writer.write() and writer.close() an Uncaught (in promise) appears alongside the real error.
- [23]
Of the three capabilities required, two were written by hand (the ZIP central directory walk and the XML tag scanner), and one of those two replaces a facility the browser does provide (DOMParser).
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toReading .xlsx in the browser without a spreadsheet library
1 article · August 16, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.