Skip to content

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

How we use AISend a correction

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
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [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. [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.

    ReportedSupportedView cited source
  3. [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.

    ReportedSupportedView cited source

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · August 16, 2026

    Reading .xlsx in the browser without a spreadsheet library

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

Entities

Loading related stories