postHostMessageAsync never times out — infinite loading when iframe host is slow to respond
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.3k
- Forks
- 641
- Avg merge
- 12h 4m
- Merged PRs (30d)
- 57
Description
Describe the bug
When the editor is embedded as an iframe with allowParentController (controller mode), postHostMessageAsync in pxteditor/editorcontroller.ts creates a Promise that only resolves when the host posts back a pxthost message with a matching id. There is no timeout and no reject path anywhere in the codebase, so if the host is slow to respond or drops the message, the Promise stays pending forever.
Because workspace.initAsync() → syncAsync() → iframeworkspace.listAsync() calls postHostMessageAsync({ action: "workspacesync", response: true }), the entire bootstrap .then chain in webapp/src/app.tsx halts. The #loading overlay (defined statically in webapp/public/index.html) is only removed near the end of that chain, so users see an infinite loading spinner.
The existing fallback syncAsync().catch(() => switchToMemoryWorkspace(...)) at webapp/src/workspace.ts:~1692 is already wired to recover from a rejection, but never triggers because no rejection is ever produced.
To Reproduce
- Open any MakeCode editor embedded as an iframe in a host that implements the workspacesync / workspaceloaded protocol
- Open Chrome DevTools → Network tab → set throttling to Slow 4G
- Reload the page
- The loading spinner remains indefinitely; the editor never mounts
- Switch throttling back to No throttling → the editor eventually loads as the delayed host response arrives
Expected behavior
postHostMessageAsync should reject pending requests after a reasonable timeout (e.g. 30s), allowing the already-present switchToMemoryWorkspace fallback in syncAsync to take over so the editor at least boots with an in-memory workspace instead of hanging forever.
Verification of fix direction
Verified in-browser without redeploying by attaching a DevTools conditional breakpoint on the pendingRequests[env.id] = { resolve, reject } line with this
expression:
(function(){var id=env.id,a=env.action;setTimeout(function(){var r=pendingRequests[id];if(r){console.warn("[POSTHOST TIMEOUT]",a,id);delete
pendingRequests[id];r.reject(new Error("host no-response: "+a));}},15000);return false;})()
Under Slow 4G reproduction, both workspacesync and workspaceloaded time out at 15s, the syncAsync catch triggers, memory workspace fallback kicks in, and the
editor renders normally. Confirms that adding a timeout alone is sufficient to unblock the UI.
Proposed fix (sketch)
Add a timeout in postHostMessageAsync when msg.response === true:
const HOST_MESSAGE_TIMEOUT_MS = 15000; // or configurable
if (msg.response) {
pendingRequests[env.id] = { resolve, reject };
setTimeout(() => {
const req = pendingRequests[env.id];
if (req) {
delete pendingRequests[env.id];
pxt.warn(`postHostMessageAsync timeout: ${env.action} (${env.id})`);
req.reject(new Error(`host no-response: ${env.action}`));
}
}, HOST_MESSAGE_TIMEOUT_MS);
}
Happy to open a PR if the direction is acceptable. Open questions for the maintainers:
- Default timeout value (30s suggested to accommodate legitimately slow host workflows like OAuth)
- Whether to expose it as a per-request opt-out (e.g. msg.timeout, 0 = no timeout to preserve current behavior for hosts that need it)
- Whether to apply the same treatment to iframeDriver.sendRequest for symmetry
Desktop (please complete the following information):
- OS: win11
- Browser : chrome 147.0.7727.102
- Version : pxt-core 12.3.5
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read pxteditor/editorcontroller.ts to trace postHostMessageAsync and its pending request handling, then inspect the syncAsync fallback around webapp/src/workspace.ts:~1692. Check how workspace.initAsync flows through webapp/src/app.tsx and the loading overlay in webapp/public/index.html. Done means a missing host response rejects after the chosen timeout and the in-memory workspace fallback lets the editor boot.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 70/100