Repository navigation
Description
The interactive iframe's service worker cannot serve SVG, MP3 or MP4 static assets, though the bundler supports importing all three. instrument-bundler loads .svg, .mp3 and .mp4 with esbuild's dataurl loader, which (esbuild 0.28) produces data:image/svg+xml,<svg …> (percent-encoded text, not base64), data:audio/mpeg;base64,… and data:video/mp4;base64,…. worker.js only accepts data:image/*;base64,… and data:text/*,…, so it throws for all three. The throw happens before respondWith is called, so the request falls through to the network, which has no such file. An author who writes import tone from './tone.mp3' and staticAssets: { '/tone.mp3': tone }, as the instrument guidelines describe, gets audio that never plays and SVG stimuli that never appear, with nothing reported to the host.
Where
packages/runtime-internal/src/interactive/worker.js:22-51:
function dataUrlToResponse(dataUrl) {
// Try base64 image format first
const base64Match = /^data:(image\/[a-zA-Z+]+);base64,([A-Za-z0-9+/=]+)$/.exec(dataUrl);
// ...
// Try plain text format
const textMatch = /^data:(text\/[a-zA-Z+]+),(.*)$/.exec(dataUrl);
// ...
throw new Error('Invalid or unsupported data URL: must be a base64-encoded image or plain text');
}and the loader that produces those URLs, packages/instrument-bundler/src/utils.ts:24:
.with(P.union('.jpeg', '.jpg', '.png', '.svg', '.webp', '.mp3', '.mp4'), () => 'dataurl' as const)Reproduce
- In the playground, copy the "Interactive Instrument with Static Assets" example and add a
tone.mp3and anicon.svg. - Import both (
import tone from './tone.mp3',import icon from './icon.svg'), add them tostaticAssetsas'/tone.mp3'and'/icon.svg', and reference them fromfragment.html(<audio src="tone.mp3" controls>,<img src="icon.svg">). - Open the instrument in
apps/web.
Actual: the service worker logs Invalid or unsupported data URL: must be a base64-encoded image or plain text for each request, both requests go to the server and fail, the audio element cannot play and the SVG image is broken. The PNG in the same instrument loads.
Expected: every data URL the bundler can produce for an import is served with its own MIME type.
Tests
packages/runtime-internal/src/interactive/__tests__/worker.test.ts: it('should serve every data URL the bundler emits, so svg, mp3 and mp4 static assets load'). Use the exact forms esbuild produces: data:image/svg+xml,<svg …>, data:audio/mpeg;base64,… and data:video/mp4;base64,…. Assert the Content-Type and the decoded body of each.
e2e, in testing/src/specs/instrument-completion.spec.ts (or a playground spec): render an interactive instrument whose staticAssets include an SVG and assert the image's naturalWidth is greater than 0.
Suggested fix
Parse any data:<mime>[;base64],<payload> generically. Take the MIME type from the URL, base64-decode when ;base64 is present, and otherwise decodeURIComponent the payload (esbuild's text form is percent-escaped). Then respond with those bytes. Keep throwing only for strings that are not data URLs at all.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e).dataUrlToResponse(worker.js:22-51) and thedataurlbranch ofinferLoader(utils.ts:24) are unchanged, and no open PR touchesworker.js.decodeURIComponentwould still break SVGsesbuild's percent form escapes only tab, CR, LF,
#, trailing whitespace, and a%that already starts a hex escape (dataurl.go@ v0.28.2). Every other%is left as is. With esbuild 0.28.2,<svg width="100%" fill="#f00">becomesdata:image/svg+xml,<svg width="100%" fill="%23f00">, anddecodeURIComponentthrowsURIError: URI malformedon100%". The suggested fix would still fail for any SVG with a percentage length or gradient offset.The Fetch spec's
data:URL processor percent-decodes leniently and handles;base64and MIME parameters. Let it do the decoding:new Response(blob)takes itsContent-Typefrom the blob. It has no URL of its own, so the page still sees the request URL, as it does today.respondWith. A bad value then fails that one request as a network error instead of falling through to the server.fetchdecodesdata:URLs too, andworker.test.tsruns in the node environment, so the unit tests exercise the real decoding.connect-srcleaves outdata:would block thisfetch.MP4 is not always
video/mp4esbuild's MIME table has no
.mp4(mime.go@ v0.28.2), so it falls back to Go'shttp.DetectContentType. That returnsvideo/mp4only when theftypbox lists anmp4*brand. The playground'scat-video.mp4does (mp42). Aqtfile, or anisom iso2 avc1file withoutmp41, comes out asdata:application/octet-stream;base64,…. The worker above serves that as is. Forcingvideo/mp4would mean the bundler sets the type itself, which is a separate change.Tests
worker.test.ts: use the forms above, with a bare%and a%23in the SVG and anapplication/octet-streamMP4.respondWithnow receives a promise, soawaitit in the existing tests.'should throw for an unsupported data URL'(:101-104) usesdata:application/json,{}, which becomes valid. Replace it with a non-data:value and assert that the promise rejects.instrument-completion.spec.tswould need an instrument withstaticAssetsinapps/web, and no built-in instrument has one. Usetesting/src/specs/playground.spec.tsinstead, next to the upper-case PNG share-link test (:188). That test setsimage.srcto the data URL, so it never reaches the worker. Puticon.svgin the link as plain text: the playground base64-decodes only files thatinferFileTypemaps toasset, and.svgmaps tohtml(file.ts:20,:30-36). DeclarestaticAssets: { '/icon.svg': icon }and put<img src="icon.svg">inhtml. Give the root<svg>a fixedwidth/height, put the%and the#on a child, and assertnaturalWidth > 0.Other notes
<audio>/<video>Rangerequests with a 200 that carries the whole body. Safari has been reported to need a 206 from a service worker for media (write-up). Playwright runs only chromium and firefox, so check MP3/MP4 in Safari by hand, and file the 206 work separately if it fails there.worker.jschange, so it stays off the bundlerAGENTS.mdlist of files to change together. Say inpackages/runtime-internal/AGENTS.mdthat the worker serves anydata:URL.packages/instrument-guidelines/AGENTS.md(:516,:542,:697) calls imported assets base64 data URLs, which an SVG is not. That file is a published package, so fixing the wording is a release of its own.playground.spec.ts.@opendatacapture/vite-plugin-runtime.Triage note written with Claude Code. Code references point at
main@d86740e.