Skip to content

Commit a5890e2

Browse files
committed
fix: prevent broken script execution during streaming in widget iframe
Three bugs in the script execution code caused interactive widgets (BFS/DFS graphs, animations) to fail: - btoa() output contains =, +, / which are invalid in HTML attribute names, causing setAttribute to throw and abort the entire forEach - Incomplete <script> tags during streaming produced broken fragments that failed on execution - No error isolation meant one bad script blocked all others Fix: defer script execution until all <script> tags are fully closed, sanitize base64 hashes for attribute names, skip empty scripts, and wrap individual executions in try/catch.
1 parent a44c0d5 commit a5890e2

File tree

1 file changed

+24
-14
lines changed

1 file changed

+24
-14
lines changed

apps/app/src/components/generative-ui/widget-renderer.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,13 @@ window.addEventListener('message', function(e) {
361361
if (!content) return;
362362
363363
// Strip script tags from HTML before inserting — scripts are handled separately below
364+
var rawHtml = e.data.html;
364365
var tmp = document.createElement('div');
365-
tmp.innerHTML = e.data.html;
366+
tmp.innerHTML = rawHtml;
366367
var incomingScripts = [];
368+
var scriptOpens = (rawHtml.match(/<script[\\s>]/gi) || []).length;
369+
var scriptCloses = (rawHtml.match(/<\\/script>/gi) || []).length;
370+
var allScriptsClosed = scriptOpens <= scriptCloses;
367371
tmp.querySelectorAll('script').forEach(function(s) {
368372
incomingScripts.push({ src: s.src, text: s.textContent });
369373
s.remove();
@@ -410,19 +414,25 @@ window.addEventListener('message', function(e) {
410414
content.innerHTML = tmp.innerHTML;
411415
}
412416
413-
// Execute only new scripts (not previously executed)
414-
incomingScripts.forEach(function(scriptInfo) {
415-
var key = scriptInfo.src || scriptInfo.text;
416-
if (content.getAttribute('data-exec-' + btoa(key).slice(0, 16))) return;
417-
content.setAttribute('data-exec-' + btoa(key).slice(0, 16), '1');
418-
var newScript = document.createElement('script');
419-
if (scriptInfo.src) {
420-
newScript.src = scriptInfo.src;
421-
} else {
422-
newScript.textContent = scriptInfo.text;
423-
}
424-
content.appendChild(newScript);
425-
});
417+
// Execute only new scripts — skip entirely while a <script> tag is still streaming
418+
if (allScriptsClosed) {
419+
incomingScripts.forEach(function(scriptInfo) {
420+
var key = scriptInfo.src || scriptInfo.text;
421+
if (!key || !key.trim()) return;
422+
var hash = btoa(unescape(encodeURIComponent(key))).slice(0, 16).replace(/[^a-zA-Z0-9]/g, '');
423+
if (!hash || content.getAttribute('data-exec-' + hash)) return;
424+
content.setAttribute('data-exec-' + hash, '1');
425+
try {
426+
var newScript = document.createElement('script');
427+
if (scriptInfo.src) {
428+
newScript.src = scriptInfo.src;
429+
} else {
430+
newScript.textContent = scriptInfo.text;
431+
}
432+
content.appendChild(newScript);
433+
} catch(e) {}
434+
});
435+
}
426436
reportHeight();
427437
}
428438
});

0 commit comments

Comments
 (0)