Skip to content

Update bundled Deno from 2.7.14 to 2.9.x #15157

Open

Description

@cderv

Quarto 1.11 moves the bundled Deno from 2.7.14 to the 2.9 line (2.9.8 at time of writing). This issue tracks the update and every Deno-related change it requires, so we can decide later what needs to be backported to 1.10.x.

The main reason for doing it now is security. Deno 2.9.8 distrusts the compromised eMudhra / emSign intermediates (denoland/deno#37046), and 2.7.14 still accepts certificates chaining to them. Details in #15152. Any 2.9.x before 2.9.8 does not have the fix, so 2.9.8 is the minimum.

Changes required by the update

  • deno install refuses permission flags outside a global install (-g) since Deno's CLI parser rewrite (refactor: remove clap from the tree denoland/deno#36489). Our vendoring scripts call deno install --allow-all --entrypoint ..., which 2.7.14 accepts and 2.9.8 rejects with Permission flags can only be used in a global setting. The scripts continue on error, so configure finishes but no dependency is vendored and the deno_cache stays empty. Passing --allow-import instead, which is the only permission flag the non-global form still accepts, vendors all four entrypoints.
    $DENO_BIN_PATH install --allow-all --no-config --entrypoint $entrypoint "--importmap=$QUARTO_SRC_PATH/import_map.json"

    CALL !DENO_BIN_PATH! install --allow-all --no-config --entrypoint %%E --importmap="!QUARTO_SRC_PATH!\import_map.json" || (
  • Deno 2.9.8 bundles TypeScript 6.0, which reports new type errors in our code. As deno test type-checks each test's import graph, they fail the whole test suite. Three kinds: import assertions (assert: { type: "json" }) must become import attributes (with), fetch no longer accepts a Uint8Array<ArrayBufferLike> body (Posit Connect Cloud publisher), and setTimeout is now typed as returning Node's Timeout instead of number (two tests). The fixes also type-check with Deno 2.7.14.
  • conda-forge has no Deno 2.9.8 build yet (latest is 2.9.5), so the RHEL tarball job stays disabled, see Add back RHEL7 build when Deno 2.4.5 is available in conda-forge (or not) #13409.

I'll add here any other Deno-related change found during the update or after it ships.

Backport

The security fix only matters for 1.10.x if 1.10.x also gets Deno 2.9.8. In that case, the vendoring script change and the type fixes above need to go with it.

Related: #15152, #14504 (previous Deno update to 2.7.14)

Activity

  1. added this to the v1.11 milestone on Oct 22, 2026
  2. cderv commented on Oct 22, 2026

    @cderv
    MemberAuthor

    Found a blocking problem with this update: Deno KV values written by Deno 2.9.8 can't be read by Deno 2.7.14, reading them fails with RangeError: could not deserialize value. The other direction works, 2.9.8 reads data written by 2.7.14.

    Quarto uses Deno KV for two caches:

    • the project cache in .quarto/project-cache/
      async init(): Promise<void> {
      const indexPath = join(this.projectScratchDir, "project-cache");
      ensureDirSync(indexPath);
      this.index = await Deno.openKv(join(indexPath, "deno-kv-file"));
    • the Sass cache in the user cache directory (%LOCALAPPDATA%\quarto\sass on Windows, ~/Library/Caches/quarto/sass on macOS, ~/.cache/quarto/sass on Linux), which is shared by every Quarto version installed on the machine

      quarto-cli/src/core/sass.ts

      Lines 409 to 411 in 517944d

      const cacheDir = useSessionCache
      ? join(temp.baseDir, "sass")
      : quartoCacheDir("sass");

    I tested on Windows with a dev build on Deno 2.9.8 and Quarto 1.10.19 (Deno 2.7.14):

    • Render a project with the dev build, then with 1.10.19: ERROR: RangeError: could not deserialize value.
    • Render a single document with the dev build, then another single document with 1.10.19: same error, coming from the shared Sass cache. 1.10.19 with a fresh cache renders fine.
    • Deleting .quarto/ or the sass cache folder fixes it.
    • This is new with Deno 2.9. Quarto 1.9.38 (Deno 2.4.5) reads caches written by 1.10.19 without problem.

    So as soon as a Quarto with Deno 2.9.8 has rendered on a machine, an older Quarto on the same machine (for example the one bundled with RStudio or Positron) fails to render projects, and HTML documents through the Sass cache, until the cache is deleted. Same for a 1.10.x backport: older 1.10.x installs would break once the patch release has run. The QUARTO_DENO workaround from #15152 does it too: 1.10.19 with QUARTO_DENO pointing to Deno 2.9.8, then 1.10.19 without it, gives the same error.

    This needs a solution before we ship Deno 2.9.8. We could store the KV files under a name or folder that depends on the Deno version, so older versions never open them. Treating a deserialize error as a cache miss would only help versions that have this fix, not the ones already released.

    Deno only reproduction
    // write.ts
    const kv = await Deno.openKv(Deno.args[0]);
    await kv.set(["version"], "1.11");
    kv.close();
    // read.ts
    const kv = await Deno.openKv(Deno.args[0]);
    console.log((await kv.get(["version"])).value);
    kv.close();
    deno-2.9.8 run --unstable-kv -A write.ts kv-file
    deno-2.7.14 run --unstable-kv -A read.ts kv-file
    # RangeError: could not deserialize value
  3. cderv commented on Oct 23, 2026

    @cderv
    MemberAuthor

    The KV cache problem from my previous comment is fixed by #15180, which keeps the KV stores in a folder per V8 version. I checked it with the update branch rebased on it and Quarto 1.10.19, rendering in both orders, for a project and for a single document. All renders work, and each version keeps its own cache.

    Going through the Deno 2.8 and 2.9 changes, I found three other behaviour changes that affect us.

    Network errors lose their reason

    Since denoland/deno#35618, a failed fetch throws TypeError: fetch failed, and the actual reason is only in error.cause. We only print error.message, so users don't see the host or the reason anymore. For example with quarto add https://expired.badssl.com/x.zip:

    # Quarto 1.10.19 (Deno 2.7.14)
    ERROR: TypeError: A network error occurred when attempting to inspect the extension 'https://expired.badssl.com/x.zip'. Please try again.
    
    error sending request for url (https://expired.badssl.com/x.zip): client error (Connect): invalid peer certificate: certificate expired: ...
    
    # dev build (Deno 2.9.8)
    ERROR: TypeError: A network error occurred when attempting to inspect the extension 'https://expired.badssl.com/x.zip'. Please try again.
    
    fetch failed
    

    quarto-cli/src/core/log.ts

    Lines 360 to 366 in 714c426

    export function logError(e: unknown, defaultShowStack = true) {
    // normalize
    const err = asErrorEx(e, defaultShowStack);
    // print error name if requested
    let message = err.printName ? `${err.name}: ${err.message}` : err.message;

    This matters for this update in particular: a user behind a server using the distrusted eMudhra intermediates would get a certificate error, and only see fetch failed. We could print cause in logError.

    Port 4190 is now blocked by fetch

    Deno 2.9 blocks the Fetch standard bad ports (denoland/deno#34863). Our isPortSafe list has 4160 for sieve, but the sieve port in the Fetch list is 4190, so our random port picker can still return 4190.

    function isPortSafe(port: number): boolean {
    // cf https://superuser.com/a/188070
    // and netwerk/base/nsIOService.cpp in Firefox's source
    //
    // excludes port numbers that Chrome or Firefox consider unsafe
    return ![
    3659, // apple-sasl / PasswordServer
    4045, // lockd
    4160, // sieve
    5060, // sip
    5061, // sips
    6000, // X11
    6566, // sane-port
    6665, // Alternate IRC [Apple addition]
    6666, // Alternate IRC [Apple addition]
    6667, // Standard IRC [Apple addition]
    6668, // Alternate IRC [Apple addition]
    6669, // Alternate IRC [Apple addition]
    6679, // osaut
    6697, // IRC + TLS
    7000, // As of 2024-10-21, Chrome doesn't appear to like this port anymore

    Chrome uses 9222 when it is free, otherwise a random port between 3000 and 8000. If that port is 4190, the readiness check fetch on it will always fail, until the launch times out.

    const port = options.port ?? findOpenPort(9222);

    const response = await fetch(`http://localhost:${port}/json/list`, {

    With a server listening on 4190, fetch("http://localhost:4190/json/list") returns 200 with Deno 2.7.14, and fails with Requests to port 4190 are blocked with Deno 2.9.8. I did not reproduce it with Chrome itself. All the other bad ports between 3000 and 8000 are already in our list.

    quarto run keeps deleting its Deno cache

    Before running a script, quarto run checks that every file of the bundled deno_std cache is also in the user cache, and if not it deletes the user cache and copies it again.

    if (
    fileList.some(filesDiffer) ||
    // perf: don't check contents; instead only look at filenames, since they're all hashed anyway
    !directoryListingsMatch(distDenoStd, cacheDenoStd)
    ) {
    removeIfExists(cacheDenoStd);
    copyTo(distDenoStd, cacheDenoStd);
    }

    The bundled cache has SQLite -shm and -wal files, and Deno 2.9.8 removes node_analysis_cache_v2-shm after a run. So the next quarto run deletes the cache, including the npm packages the script downloaded. With QUARTO_RUN_NO_NETWORK=true, a script importing an npm: package fails right after an online run:

    npm package not found in cache: "lodash-es", --cached-only is specified.
    

    Quarto 1.10.19 keeps its cache and runs the same script offline fine. Deno 2.9.8 alone, with its own DENO_DIR, also works offline, so this is the filename comparison in our code meeting a different SQLite cleanup in the new Deno.

    These three come with the Deno update, so they'll need to go with it, including in a 1.10.x backport if we do one. I am still checking the remaining items (test sanitizers now off by default in deno test, deno bundle for quarto call build-ts-extension, PDF test helpers, preview).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions