workers-sdk
workers-sdk copied to clipboard
Vite plugin has race condition with HMR - `A hanging Promise was canceled.`
What versions & operating system are you using?
4.19.1 [wrangler], 4.7.7 [hono], 7.5.2 [react-router] (but have tried many versions of each)
Please provide a link to a minimal reproduction
No response
Describe the Bug
I reported a bug in the templates repo, but I was told that this might be the better spot for it. I believe there is a race condition in the vite plugin where if two events fire close together you can get an error below. I have also since found several individuals who also run into the issue, which I think helps point at a problem in the plugin.
A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all code and events related to the Promise's I/O context have already finished.
https://github.com/cloudflare/templates/issues/600
Here's the text from that issue since it's the best description of the issue. I think the key is that I can more or less trigger this in the cloudflare template.
This is a bit of a weird one, but I have spent maybe 20+ hours trying to get to the bottom of the source and so I'm pretty sure on most of this stuff: <3
In my app, if two saves happen in fast succession, then it will very commonly (like 1 out of 4 times) trigger the following error.
At first I assumed I had a dangling promise somewhere in my app, but peeled back every file until I had none left and was still able to trigger it. So I attempted to reproduce it directly in the hono-react-router template and after a little bit of wiggling, I was able to do it.
However, in the template app, it's very difficult to make happen. I am fairly certain that this is because the app builds so quickly, that it can never get into a state where two HMR reloads cross over each other (at least on my M3 macbook pro). So in order to try to trigger it I changed the template app in 1 way, which is to just load any largeish library. I chose @adobe/spectrum since it has a lot of components, but also confirmed I could make it happen with other things like radix/dummy files.
My current best assumption is that the following code is not guaranteed to work when two HMR events occur very close together:
// app.ts from the hono-react-router template
app.get("*", (c) => {
const requestHandler = createRequestHandler(
() => import("virtual:react-router/server-build"), // <-- this line is my best guess
import.meta.env.MODE,
);
return requestHandler(c.req.raw, {
cloudflare: { env: c.env, ctx: c.executionCtx },
});
});
If I had to do some conjecture, in some cases the app boots up, but before the createRequestHandler function invokes that dynamic import, a new server-build exists in that spot.
In order to make the error happen in a small app like the demo template, you may have to press and hold cmd+s on the welcome.tsx file, or hit it really fast in bursts, however in our real life app, I believe many of our developers run into this issue on every few saves (which kills the entire dev server) because their editors probably save once when they hit save, and then save again after formatting is applied.
I'm sorry I don't have a minimum reproduction linked, but I'm happy to get on a call at any time and show the error in practice and in the template project with my changes.
Please provide any relevant error logs
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (ssr) page reload app/welcome/welcome.tsx
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (__react_router_css_dev_helper__) page reload app/welcome/welcome.tsx
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (ssr) page reload app/welcome/welcome.tsx
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (__react_router_css_dev_helper__) page reload app/welcome/welcome.tsx
A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all code and events related to the Promise's I/O context have already finished.
5:35:00 PM [vite] Internal server error: The script will never generate a response.
at async ProxyServer.fetch (file:///Users/alex/code/test-template/wispy-pond-2aa3/node_modules/.pnpm/[email protected]/node_modules/miniflare/src/workers/core/proxy.worker.ts:173:11)
Warning: A promise was resolved or rejected from a different request context than the one it was created in. However, the creating request has already been completed or canceled. Continuations for that request are unlikely to run safely and have been canceled. If this behavior breaks your worker, consider setting the `no_handle_cross_request_promise_resolution` compatibility flag for your worker.
at /Users/alex/code/test-template/wispy-pond-2aa3/app/root.tsx:15:393
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (client) hmr update /app/welcome/welcome.tsx, /app/app.css
5:35:00 PM [vite] (ssr) page reload app/welcome/welcome.tsx
Oh! I should add that I did attempt to simply throttle the vite hmr events like so:
server: {
watch: {
awaitWriteFinish: {
stabilityThreshold: 200,
pollInterval: 100,
},
},
},
But unfortunately this didn't resolve the issue in my testing. It just somewhat changes the exact set of things that will trigger it. I do think this may be a temporary solution for folks who have editors that save files twice though, but it seems like there's gotta be some way to make the request more stable through the request into react-router.
One more thing that isn't necessarily obvious from the single log output is that there is a line number for the error in the logs:
/Users/alex/code/test-template/wispy-pond-2aa3/app/root.tsx:15:393
But this is random as far as I can tell. Each time it'll give me a different file and line number. Often on lines that are comments or could never error. Many times it's in dependencies like react, etc.
My best guess is that the asynchronous event that fires to cause this just chooses whatever line happened to be executing at the time.
Please provide any relevant error logs
No response
thanks for picking up this ticket @threepointone @jamesopstad i'm available most US work days (i can probably make some UK times work too) if you want any more explicit demos of the issue. Happy to be as helpful as I can.
Hi @SlexAxton. I've spent some time trying to reproduce this and haven't been able to. I used the react-router-hono-fullstack-template and added @adobe/react-spectrum like you suggested. Please could you try and create a minimal reproduction. We won't be able to fix this unless we can reproduce it consistently.
Just as an idea, could you try pulling createRequestHandler outside of the handler:
// app.ts from the react-router-hono-fullstack-template
const requestHandler = createRequestHandler(
() => import("virtual:react-router/server-build"),
import.meta.env.MODE,
);
app.get("*", (c) => {
return requestHandler(c.req.raw, {
cloudflare: { env: c.env, ctx: c.executionCtx },
});
});
It might not make any difference but is worth a go (it seems a bit odd that this template only delegates GET requests to react-router but that's not relevant here).
I'll try and get a more solid reproduction in the next hour or so. Unfortunately it reproduces in my actual app like every 5-10 saves :(.
Re: createRequestHandler outside of the function, I actually initially had it there based on y'all's template from a few weeks ago. Then before I created this issue I went to see if y'all had any changes and saw that it moved into the function. I thought maybe this was the secret trick that would stop the problem from happening, but there wasn't a change in outcome.
Re: GET - it happens to be fine for us since we're not using loaders atm, but I did notice a ticket for that issue already when I was looking for other issues that matched mine: https://github.com/cloudflare/templates/issues/434 - so it's known.
Back soon!
OK I think I have something that reproduces easier but still not as good as I can probably get it, but sharing it here just to get you something asap.
Clone this repo:
https://github.com/SlexAxton/cf-bug-repro-9518
pnpm install
pnpm run dev
Then open app/welcome/welcome.tsx in cursor or vscode.
And press and hold cmd+s in bursts, and watch the console output while you do. Eventually you may get the error I screenshotted above. I'll keep working on making it more likely to happen.
Here's a video of me reproing it in this repo with these steps. Sometimes it happens quickly. This time wasn't super quick.
https://github.com/user-attachments/assets/4ff5c0d8-8cef-4cb7-b78a-32c4330def38
I know it seems really 'not real world' to hold cmd+s but i promise in my workplace app no one is holding save, our setup just really seems to make the race condition much more likely to occur.
Anecdotal but I also find that if I leave my app server running for 10 minutes while I do something else and then come back and make a change to a file and save it, it'll almost always happen on the first save after the delay. And once it fires, you have to reboot the entire server from scratch.
Just to hop on here - We have very similar behaviour happening also - every 3-5 saves as @SlexAxton does.
Unsure if it's helpful information but we have a couple of Cloudflare / React-router projects and I'm pretty sure it happens more often on the larger projects.
I'm pretty snowed right now but my next step would be to have it run inside of a "cpu throttled" docker container to see if it is consistently reproducible there.
Thanks @SlexAxton for the reproduction. We are looking into this and will update here when we have more info.
Thanks @SlexAxton for this comprehensive write up and repro I've noticed this a bunch in my projects but never went in depth like you did to identify and reliably repro this Hoping it's fixed soon
I think I've identified the issue. In Vite's ModuleRunner, if there are concurrent requests for the same module then the first request promise is reused (https://github.com/vitejs/vite/blob/3f469012ad38e3cb330adc74a8b3ec88561c822e/packages/vite/src/module-runner/runner.ts#L233-L251). When this happens it violates a constraint of the Workers runtime that I/O objects can't be shared between invocations (https://developers.cloudflare.com/workers/observability/errors/#cannot-perform-io-on-behalf-of-a-different-request).
Figuring out the best way to solve this will take a bit of time but for now I've put up a draft PR that overrides Vite's request caching (https://github.com/cloudflare/workers-sdk/pull/9693). Please could you try out this prerelease of @cloudflare/vite-plugin and confirm if it solves the issue for you:
npm i https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7
Thanks!
I upgraded all vite and cloudflare/wrangler and react router deps to current latest and then swapped my plugin over to this and I believe that it's likely fixed.
We'll dev with it going forward and I'll let you know if one of my coworkers has a different experience.
Thank you so much for taking such a race-conditiony issue seriously. This makes our lives 1000x better. :pray:
No problem. These kind of bugs are the most annoying for the users experiencing them and also the hardest to track down and fix. Glad we're getting somewhere and that you're (hopefully!) unblocked while we work on a more robust fix.
having this issue too, just updated via npm i https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7 ---
Will see what else i can do if it fixes it
only happens on vite dev mode.. makes dev on local incredibly frustrating!
nternal server error: The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/
at async ProxyServer.fetch (file:///Users/rob/dev/tracked/app/node_modules/miniflare/src/workers/core/proxy.worker.ts:173:11) (x3)
turned off HMR for now ..
only happens on vite dev mode.. makes dev on local incredibly frustrating!
nternal server error: The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/ at async ProxyServer.fetch (file:///Users/rob/dev/tracked/app/node_modules/miniflare/src/workers/core/proxy.worker.ts:173:11) (x3)turned off HMR for now ..
@rdvo To clarify, are you saying that the issue still occurs for you when using "@cloudflare/vite-plugin": "https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7"?
yes it does. every few saves i get that error
Using "@cloudflare/vite-plugin": "https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7" works for me, I no longer see the issue happening.
yes it does. every few saves i get that error
@rdvo Are you able to provide a reproduction? We're working on fixing this properly but need to ensure we've identified the correct cause. I haven't seen any other reports of the error still being present with the prerelease.
I'm using React Router 7, ShadCN (latest) , Tailwind(Latest), Clerk Auth, The Vite plugin you referenced bove
{
"name": "react-router-hono-cloudflare",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"cf-typegen": "wrangler types",
"deploy": "npm run build && wrangler deploy",
"dev": "react-router dev",
"preview": "npm run build && vite preview",
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
},
"dependencies": {
"@clerk/backend": "^2.3.0",
"@clerk/react-router": "^1.6.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hono/clerk-auth": "^3.0.1",
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tabler/icons-react": "^3.34.0",
"@tanstack/react-table": "^8.21.3",
"check-password-strength": "^3.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"hono": "^4.7.11",
"isbot": "^5.1.27",
"lucide-react": "^0.523.0",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-day-picker": "^9.7.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.58.1",
"react-router": "^7.6.3",
"recharts": "^3.0.0",
"shadcn": "^2.7.0",
"sonner": "^2.0.5",
"svix": "^1.68.0",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
"zod": "^3.25.67"
},
"devDependencies": {
"@cloudflare/vite-plugin": "https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7",
"@react-router/dev": "^7.5.3",
"@tailwindcss/vite": "^4.1.10",
"@types/node": "^20.19.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4",
"wrangler": "4.22"
}
}
@SlexAxton and others who have experienced this issue, please could I ask two things of you?
- Confirm if you have experienced the issue since using the https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7 release linked above.
- Use a normal
@cloudflare/vite-pluginrelease (1.8.0 is the latest) and add the following code to your Worker entry file (normallyworkers/app.ts).
if (import.meta.hot) {
import.meta.hot.accept();
}
Then confirm if this resolves the issue or if it still persists.
Thanks. Really appreciate your help with resolving this.
@jamesopstad we were affected by this issue, and it seems that:
- Applying the change from https://pkg.pr.new/@cloudflare/vite-plugin@4b45ae7 has fixed the issue (running for 2 days for the whole team now with no problems).
- Not sure if noteworthy, but for whatever reason using this
https://dependency in our pnpm workspace monorepo did not work correctly and ended up in a weird peer dep situation with wrangler, so instead I applied the same commit as apnpm patchand it's working for us.
- Not sure if noteworthy, but for whatever reason using this
- The
import.meta.hot.accept();fix on top of unpatched 1.8.0 also seems to resolve this issue for me, though with a slightly less exercising -- tested for the last few hours, usually I would have seen about 20 crashes in this time.
Thanks for addressing this! It's been a real source of pain for us.
@jamesopstad same here, we have been having this issue for a couple of weeks.
-
I can't try the suggested package right now because we are on Vite 7 and the plugin at that point did not support #9773 .
-
The
import.meta.hot.accept()on 1.9.0 works very well and also seems to fix an issue I had where Vite could never fast refresh, with messagehmr invalidate *filename* Could not Fast Refresh.
Hope it helps, and thanks for the good work! I'll let you know if I see anything worth noting.
@jamesopstad This has been driving me nuts for the longest time. The two changes above have made this considerably more stable, and also changes propagate much, much faster. import.meta.hot.accept() wasn't in the original documentation either.
Thanks for looking into this and getting a fix out in such a short space of time.
I've opened a PR (https://github.com/cloudflare/workers-sdk/pull/10001) to inject the HMR code in the Worker entry file so that users don't have to add it themselves. ~~That will close this issue and I'll update here when it's released. Feel free to reopen the issue if problems still persist though.~~
Also, just wanted to say, if you are experiencing a frustrating dev experience then please do open issues. We often don't know about things until they're reported. The Vite plugin and the Vite APIs that it uses are all quite new and we want to keep making them better. Thanks!
sadly none of the fixes here worked. i'm just disabling HMR for now. Whatever
Sorry for the noise, I created a separate issue thinking it would be different, and posted in the PR as I lost track of things, but will bring my talking point back here for consistency.
I updated to 1.9.6 of the plugin and added the HMR snippet to my worker entry. While it didn't solve my issue, it made the failure more consistent.
Our app is large. Without incremental compile, it takes 3-6 seconds to build in Vite, and AFAIK with the Cloudflare plugin the app rebundles the whole thing, even with HMR.
What I'm noticing is that if HMR works or not is entirely timing dependent (I spent the last 15-20 minutes testing to be sure).
That is, if I make a change, I'll get the HMR update. If I immediately make another change (within the next 1-5 seconds), I will see logs in my terminal and network requests done for HMR, but the update will NOT be reflected in the browser. If 5-6+ seconds have passed and I save the file again, then the page will update through HMR pretty much 100% of the time.
In the terminal, the output is pretty deterministic too: the HMR messages from Vite (the green HMR updates and yellow HMR invalidate when fast refresh doesn't work right) are the same whether it succeeds or not. But there's one key difference: when the HMR process updates my page, I will also see logs from my code (eg: console.log in my React Router loader). When I save a file and I don't get a visible change in my browser, I still see the Vite HMR logs, but I do NOT see the console.log from my React Router loader.
I don't know if that's helpful, but at least its deterministic information. Happy to test or provide any other information I can, short of giving access to my git repo.
@Phoenixmatrix To clarify, do you see the 'A hanging promise was canceled.` error highlighted in this issue? It looks from https://github.com/cloudflare/workers-sdk/issues/9963 like your issue might be a little different (though it might have the same root cause). Thanks.
I don't see the hanging promise was cancelled, no. I've actually never hit that issue. But yeah, I think the root cause is related. Sorry for the confusion! I've been a bit all over the place trying to fix this issue.
Edit: sync-ed with my team and yeah we do get that error too, though that wasn't the one I was trying to debug when I posted this. Didn't try after this change though.
@jamesopstad the change you introduced in https://github.com/cloudflare/workers-sdk/pull/10001 has created some extremely challenging and unexpected behavior for us - causing "zombie" durable object instances which remain running after an HMR update, but with no visible logging output. e.g. my client code is connecting to a "zombie websocket" i can no longer see or control short of terminating the wrangler process.
How can we opt out of the HMR? It seems extremely easy to cause subtle bugs like this, especially with DurableObjects.
@aroman Sorry it's causing you issues. There's no way to opt out at present but this is unexpected so needs resolving. Please could you open a new issue and provide some more detail (and ideally a reproduction if that's possible). Thanks!