Compare commits

...

1 Commits

Author SHA1 Message Date
Cursor Agent cacedbd03a fix: prevent custom scripts from accessing document.body before React hydration
- Add ensureBodyExists() to wait for document.body availability
- Wrap inline script content to defer execution until DOM is ready
- Add defensive checks to prevent race conditions
- Skip wrapping for scripts that already have DOM-ready checks
- Only wrap inline scripts, not external scripts with src attribute

Fixes FORMBRICKS-RW
2026-03-12 20:59:00 +00:00
@@ -41,41 +41,113 @@ export const CustomScriptsInjector = ({
if (!scriptsToInject.trim()) return; if (!scriptsToInject.trim()) return;
try { /**
// Create a temporary container to parse the HTML * Ensures document.body exists before executing the injection.
const container = document.createElement("div"); * This prevents race conditions where custom scripts try to access document.body
container.innerHTML = scriptsToInject; * before React hydration completes, which would cause:
* - React error #454 (missing document.body)
// Process and inject script elements * - TypeError: can't access property "removeChild" of null
const scripts = container.querySelectorAll("script"); */
scripts.forEach((script) => { const ensureBodyExists = (): Promise<void> => {
const newScript = document.createElement("script"); return new Promise((resolve) => {
// If body already exists, resolve immediately
// Copy all attributes (src, async, defer, type, etc.) if (document.body) {
Array.from(script.attributes).forEach((attr) => { resolve();
newScript.setAttribute(attr.name, attr.value); return;
});
// Copy inline script content
if (script.textContent) {
newScript.textContent = script.textContent;
} }
document.head.appendChild(newScript); // Wait for DOMContentLoaded
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => resolve(), { once: true });
} else {
// Document is already loaded but body doesn't exist yet (edge case)
// Use setTimeout to defer until next tick
setTimeout(() => resolve(), 0);
}
}); });
};
// Process and inject non-script elements (noscript, meta, link, style, etc.) /**
const nonScripts = container.querySelectorAll(":not(script)"); * Wraps inline script content to ensure safe execution after DOM is ready.
nonScripts.forEach((el) => { * This prevents scripts from executing before document.body is available.
const clonedEl = el.cloneNode(true) as Element; */
document.head.appendChild(clonedEl); const wrapScriptContent = (content: string): string => {
}); // Don't wrap if the script already has DOM-ready checks
if (
content.includes("DOMContentLoaded") ||
content.includes("document.readyState") ||
content.includes("window.addEventListener('load'")
) {
return content;
}
injectedRef.current = true; // Wrap the script to ensure it runs after DOM is ready
} catch (error) { return `
// Log error but don't break the survey - self-hosted admins can check console (function() {
console.warn("[Formbricks] Error injecting custom scripts:", error); if (document.readyState === 'loading') {
} document.addEventListener('DOMContentLoaded', function() {
${content}
});
} else {
${content}
}
})();
`;
};
const injectScripts = async () => {
try {
// Wait for document.body to exist before injecting any scripts
await ensureBodyExists();
// Defensive check: ensure body still exists
if (!document.body) {
console.warn("[Formbricks] document.body is not available, skipping script injection");
return;
}
// Create a temporary container to parse the HTML
const container = document.createElement("div");
container.innerHTML = scriptsToInject;
// Process and inject script elements
const scripts = container.querySelectorAll("script");
scripts.forEach((script) => {
const newScript = document.createElement("script");
// Copy all attributes (src, async, defer, type, etc.)
Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content with safety wrapper
if (script.textContent) {
// Only wrap inline scripts (not external scripts with src attribute)
if (!script.hasAttribute("src")) {
newScript.textContent = wrapScriptContent(script.textContent);
} else {
newScript.textContent = script.textContent;
}
}
document.head.appendChild(newScript);
});
// Process and inject non-script elements (noscript, meta, link, style, etc.)
const nonScripts = container.querySelectorAll(":not(script)");
nonScripts.forEach((el) => {
const clonedEl = el.cloneNode(true) as Element;
document.head.appendChild(clonedEl);
});
injectedRef.current = true;
} catch (error) {
// Log error but don't break the survey - self-hosted admins can check console
console.warn("[Formbricks] Error injecting custom scripts:", error);
}
};
injectScripts();
}, [projectScripts, surveyScripts, scriptsMode]); }, [projectScripts, surveyScripts, scriptsMode]);
return null; return null;