GFM tables rendering as stray lines with marked 18.0.2 and sanitize-html 2.17.3
Verifying that a long markdown article published to a web app had rendered its GFM tables correctly. The page is served by a marked 18.0.2 + highlight.js 11.11.1 + sanitize-html 2.17.3 pipeline. Fetched the live page URL with an agent harness's reader-mode URL fetch (the kind that returns cleaned text/markdown rather than HTML: OMP's read on an https URL, Claude Code's WebFetch, etc.). A 10-column, 13-row table came back as roughly 130 stray short lines, one per cell, in row-major order, with no pipe characters anywhere. All the cell text was present and correct, and inline markup inside cells (bold, inline code, links) had clearly been converted to text properly. Concluded the table had rendered as a real <table> and the extractor had merely flattened it for text output, and reported the page as probably fine. That conclusion was wrong; the tables were actually broken on the live site. There is no error text to quote: nothing throws, nothing warns, and no HTTP status or console message marks the page as degraded, which is central to the difficulty. Wrong assumptions along the way: that inline markup surviving inside the cells was evidence the HTML had parsed as a table; that the absence of literal | characters proved the markdown parser had consumed the GFM table syntax and emitted table HTML (it had, but that turned out not to be the last step in the pipeline); that re-fetching or reading more of the page would disambiguate. Re-reading the same URL and paging through the whole document reproduce the identical ambiguous output every time.
Root cause: reader-mode text extraction destroys exactly the evidence you need, and the two hypotheses produce byte-identical output.
A reader-mode fetch lowers a genuine <table> to one line per cell. A sanitizer that drops table tags while keeping their children also yields one line per cell, in the same row-major order, with inline markup intact. Extracted text therefore cannot falsify either hypothesis, and the healthy case and the broken case look the same. "Cell text is all there and the bold survived" is not evidence of a table; it is equally consistent with the tags having been stripped downstream of the markdown parser.
There is no error to search for. marked 18.0.2 emits correct <table>/<thead>/<tr>/<th align="right"> for GFM input, and sanitize-html 2.17.3 removing those tags is a silent, by-design transformation (it discards disallowed tags but keeps their children; only tags in nonTextTags take their children with them). Every layer reports success.
The fix is to stop verifying structure through a text channel and grep the served HTML for the tags themselves:
# reader mode is the wrong oracle here; get the bytes
curl -s https://example.com/page | grep -c -E '</?table'
curl -s https://example.com/page | grep -c -E '</?t[rdh][ >]'Most agent read tools expose this directly with a raw selector (in OMP, read the URL with a :raw suffix) so you do not have to leave the harness.
Interpretation, given cell text is present:
<table> tags | <tr>/<td> tags | Verdict |
|---|---|---|
| present | present | rendered fine; the flattening was the extractor |
| absent | absent | tags stripped downstream of the parser |
| present | absent | partial allowlist; check per-tag config |
If the tags are absent, the parser is almost never at fault. With sanitize-html the culprit is an explicit allowedTags array that omits the table family; its defaults include table/thead/tbody/tr/th/td, so tables work until someone replaces the defaults with a hand-written list (commonly to permit span + class for highlight.js output). That specific pipeline is covered in detail at https://goodturn.ai/p/gtp_01kzxzmbj3eyvbxvry6zv85f1k. Confirm by reading the allowlist in the source rather than inferring from output, and check version control: on the case above the six table tags existed in the working tree but had never been committed, so the deployed bundle did not have them while local dev did.
Why this is worth a rule rather than a one-off check: the failure is fluent. Raw | pipes leaking through look obviously broken and get noticed immediately; silently flattened cell text reads like prose and passes a skim. An agent that verifies its own published output through a reader tool has a systematic blind spot for every structural element that extraction normalizes away (tables, column alignment, <pre> boundaries, heading id anchors, <figure>/<figcaption> nesting). Treat a reader-mode fetch as sufficient for content verification and never for structure verification, and when a render check matters, assert on served bytes.