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
Describe the bug
With jsdom 30.1.0 under
environment: 'jsdom',URL.createObjectURL(blob)and aRequest/FormDatabody holding a jsdomBlobthrow:On jsdom 30.0.1,
URL.createObjectURLworks (the request body is already wrong there, see #11294). I expectedURL.createObjectURLto return ablob:URL on both.Cause. Vitest's
makeCompatBlobinpackages/vitest/src/integrations/env/jsdom.tsfinds jsdom's impl object by taking the first own symbol of aBlobinstance:jsdom 30.1.0 moved the impl from the
Symbol("impl")property into a private class field (#impl in wrapperinlib/generated/idl/utils.js). jsdom 30.1.1 keeps it there (its publishedlib/generated/idl/utils.jshas the same#impl in wrapper). ABlobinstance now has no own symbols, soimplSymbolisundefined,blob[undefined]isundefined, and reading its bytes throws. This affects every path that goes throughmakeCompatBlob:URL.createObjectURL, andRequest/FormDatabodies.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→_bytesrename (impl._bytes ?? impl._buffer), but it still looks the impl up throughimplSymbol, so it does not cover this case. #11300 is about Node 24 / undiciFileand is a different problem.Possible fix. Get the impl through jsdom's own
implForWrapperrather than scanning symbols. It is exported fromjsdom/lib/generated/idl/utils.jsin 30.1+ (older versions have it atjsdom/lib/jsdom/living/generated/utils.js). jsdom has noexportsmap, so the subpath can be required. Checked on jsdom 30.1.0: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/builddeclaresvitest: "^4.0.8"as its peer range.Reproduction
https://github.com/BigMichi1/vitest-jsdom-private-impl
package.jsonpinsvitest5.0.1 andjsdom30.1.0 and nothing else. The config and the test:vitest.config.tsblob.test.tsResults (other rows by changing the two pins):
createObjectURL"undefined"(#11294, fixed on main by #11295)TypeError … '_buffer'TypeError … '_buffer'"undefined"(#11294, fixed on main by #11295)TypeError … '_buffer'TypeError … '_buffer'Reading #11295 on main, it still fails on jsdom 30.1.x, because
implSymbolisundefinedbefore_bytes/_bufferis 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