Skip to content

Fix renders failing with RangeError when a Quarto cache is damaged - #15185

Merged
cderv merged 6 commits into
mainfrom
fix/issue-15119
Oct 23, 2026
Merged

cderv merged 6 commits into
mainfrom
fix/issue-15119

Conversation

@cderv

@cderv cderv commented Oct 23, 2026

Copy link
Copy Markdown
Member

When a Quarto cache file is damaged, quarto check and every render fail with ERROR: RangeError: could not deserialize value, with no hint at which file is at fault. In #15119 the user Sass cache (~/.cache/quarto/sass) got damaged after restoring a home folder from an external drive, and deleting that folder by hand was the only way out.

Root cause

Both caches keep their index in Deno KV: the Sass cache in src/core/sass/cache.ts and the project cache in src/core/cache/cache.ts. A kv.get() on a row whose value can't be deserialized throws, and nothing caught it, so the error ended the render. Reproduced with 1.10.19 by zeroing the value of the Sass cache version row with sqlite3.

Fix

A new src/deno_ral/kv.ts wraps every KV call of both caches and sorts errors by message, as Deno gives them no code:

  1. A damaged value (could not deserialize value, an unknown value encoding, an invalid column type) is read as a cache miss. Quarto warns once per cache file, and the write that follows the miss repairs the row. This is the ERROR: RangeError: could not deserialize value #15119 case.
  2. A damaged file (database disk image is malformed, file is not a database) fails with a single error line naming the file and the folder to delete.
  3. Any other error (locked store, unopenable path, closed handle) is rethrown with the cache path added, and never treated as damage.

Quarto never deletes a cache file itself. Deleting a store that another Quarto process has open is unsafe with SQLite, and the forms of damage found while testing are open-ended, so deleting the folder is left to the user with the error saying which one.

The new tests build really damaged stores (values zeroed with node:sqlite, truncated files, a text file in place of the database) instead of mocking errors.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

Note: autonomous AI agents submitting PRs without human oversight are not permitted — see the Code of Conduct.

Fixes #15119

cderv added 6 commits October 23, 2026 15:28
Deno KV reports a damaged cache through plain RangeError/TypeError/Error
messages with no code or distinct class, so src/deno_ral/kv.ts classifies
them by message text: a damaged value read through getKv is a miss with
one warning per file, a damaged file raises KvDamagedError naming the
folder to delete, and anything else is rethrown with the cache path.

The tests damage real stores (node:sqlite edits, truncation, a text file,
a zeroed b-tree page header) instead of mocking errors, so a Deno bump
that rewords one of these messages fails a test rather than silently
disabling the handling. printsMessage gains a count option so the
once-per-file warning can be asserted.
A damaged ["version"] row in the user-global Sass cache made every render
fail with a bare "RangeError: could not deserialize value", the error in
the report. Reading it through getKv turns the damaged value into null,
so the existing null branch of checkVersion rewrites the row: the store
repairs itself on the next render. A damaged entry row is a miss that the
following setFromHash overwrites.

A damaged file (truncated, or not SQLite) still fails the render, but the
message now names the file and the v8-<branch> folder to delete. Quarto
never deletes it: another process may hold the store, and on POSIX
SQLite later removes the -wal/-shm files by path. If the version check
fails after the store opened, the handle is closed so the folder stays
deletable on Windows.
The project cache index (.quarto/project-cache/v8-<branch>/deno-kv-file)
had the same failure shape as the Sass cache: a damaged version row or
entry aborted the render with "could not deserialize value". Routing its
KV calls through the runtime layer makes a damaged value a miss that the
next write repairs, and a damaged file an error naming the folder to
delete.

createOnDisk() now awaits its set. Without the await, the repair write
for a damaged version row ran detached, init() re-read the version
before it landed, and a malformed-page failure rejected outside
createProjectCache(). A failed init() closes the handle it opened, so a
store that opens but fails its version read stays deletable on Windows.

A locked index is not damage: it surfaces as Deno's "database is locked"
with the cache path appended.
src drops by two: both caches now open their store through
src/deno_ral/kv.ts instead of calling Deno.openKv directly. tests rise
because the damage fixtures build, damage and inspect real Deno KV
stores (Deno.openKv, truncateSync, a lock-holding child process), which
a mocked error could not replace. tests.stdImportFiles drops by one
because sass-cache.test.ts now imports its assertions from
tests/asserts.ts.
Adds the kv.ts row to the deno_ral module table and the #15119
changelog entry.
quarto preview re-resolves the cache on every re-render, so a second
resolve on the same damaged store must give the user the same
actionable message, not a different error from leftover state.
@posit-snyk-bot

posit-snyk-bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cderv
cderv merged commit 5b23163 into main Oct 23, 2026
51 of 52 checks passed
@cderv
cderv deleted the fix/issue-15119 branch October 23, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ERROR: RangeError: could not deserialize value

2 participants