Skip to content

jsdom 30.1: URL.createObjectURL and Blob request bodies throw Cannot read properties of undefined (reading '_buffer') #11336

Description

@BigMichi1

Describe the bug

With jsdom 30.1.0 under environment: 'jsdom', URL.createObjectURL(blob) and a Request/FormData body holding a jsdom Blob throw:

TypeError: Cannot read properties of undefined (reading '_buffer')

On jsdom 30.0.1, URL.createObjectURL works (the request body is already wrong there, see #11294). I expected URL.createObjectURL to return a blob: URL on both.

Cause. Vitest's makeCompatBlob in packages/vitest/src/integrations/env/jsdom.ts finds jsdom's impl object by taking the first own symbol of a Blob instance:

const implSymbol = Object.getOwnPropertySymbols(Object.getOwnPropertyDescriptors(new window.Blob()))[0]
// ...
const impl = (blob as any)[implSymbol]

jsdom 30.1.0 moved the impl from the Symbol("impl") property into a private class field (#impl in wrapper in lib/generated/idl/utils.js). jsdom 30.1.1 keeps it there (its published lib/generated/idl/utils.js has the same #impl in wrapper). A Blob instance now has no own symbols, so implSymbol is undefined, blob[undefined] is undefined, and reading its bytes throws. This affects every path that goes through makeCompatBlob: URL.createObjectURL, and Request/FormData bodies.

// own symbols on `new window.Blob(['x'])`
// jsdom 30.0.1 -> [ Symbol(impl) ]
// jsdom 30.1.0 -> []

This is a follow-up to #11294, where a comment already described the 30.1.0 private-field break after that issue was closed. #11295 fixed the _buffer → _bytes rename (impl._bytes ?? impl._buffer), but it still looks the impl up through implSymbol, so it does not cover this case. #11300 is about Node 24 / undici File and is a different problem.

Possible fix. Get the impl through jsdom's own implForWrapper rather than scanning symbols. It is exported from jsdom/lib/generated/idl/utils.js in 30.1+ (older versions have it at jsdom/lib/jsdom/living/generated/utils.js). jsdom has no exports map, so the subpath can be required. Checked on jsdom 30.1.0:

const { implForWrapper } = require('jsdom/lib/generated/idl/utils.js')
const impl = implForWrapper(new window.Blob(['hello']))
Object.keys(impl)                   // [ '_globalObject', 'type', '_bytes' ]
Buffer.from(impl._bytes).toString() // 'hello'

Keeping the symbol scan as a fallback would keep jsdom < 28 working. Could the fix also be backported to 4.x? Projects on Angular are held on Vitest 4 because @angular/build declares vitest: "^4.0.8" as its peer range.

Reproduction

https://github.com/BigMichi1/vitest-jsdom-private-impl

git clone https://github.com/BigMichi1/vitest-jsdom-private-impl
cd vitest-jsdom-private-impl
bun install
bun run test

package.json pins vitest 5.0.1 and jsdom 30.1.0 and nothing else. The config and the test:

vitest.config.ts

import { defineConfig } from 'vitest/config'

export default defineConfig({ test: { environment: 'jsdom' } })

blob.test.ts

import { expect, it } from 'vitest'

it('URL.createObjectURL accepts a jsdom Blob', () => {
  expect(URL.createObjectURL(new Blob(['hello']))).toMatch(/^blob:/)
})

it('a Blob request body keeps its content', async () => {
  const request = new Request('http://x.test/', { method: 'POST', body: new Blob(['hello']) })
  expect(await request.text()).toBe('hello')
})

Results (other rows by changing the two pins):

vitest jsdom createObjectURL Blob request body
4.1.11 30.0.1 passes fails: body is "undefined" (#11294, fixed on main by #11295)
4.1.11 30.1.0 TypeError … '_buffer' TypeError … '_buffer'
5.0.1 30.0.1 passes fails: body is "undefined" (#11294, fixed on main by #11295)
5.0.1 30.1.0 TypeError … '_buffer' TypeError … '_buffer'

Reading #11295 on main, it still fails on jsdom 30.1.x, because implSymbol is undefined before _bytes/_buffer is ever read.

System Info

System:
    OS: Linux 6.18 openSUSE Leap 15.6
    CPU: (24) x64 AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M
    Memory: 36.24 GB / 42.93 GB
    Container: Yes
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 26.10.0 - ~/.local/share/mise/installs/node/26.10.0/bin/node
    npm: 11.19.1 - ~/.local/share/mise/installs/node/26.10.0/bin/npm
    bun: 1.4.2 - ~/.local/share/mise/installs/bun/1.4.2/bin/bun
  Browsers:
    Chrome: 150.0.7871.186
    Firefox: 140.9.0esr
  npmPackages:
    vitest: 5.0.1 => 5.0.1


Also reproduced with vitest 4.1.11. jsdom versions: 30.0.1 works, 30.1.0 breaks. 30.1.1 not run, but its source stores the impl the same way.

Used Package Manager

bun

Validations

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions