vavite icon indicating copy to clipboard operation
vavite copied to clipboard

WebSocket connections are pending indefinitely

Open Vanilagy opened this issue 1 year ago • 5 comments

Hi,

I hope this issue finds you well. Great job on this library, it has simplified the tooling required to run (and build!) a full-stack application for me. However, there are still some issues that are blocking me, one is regarding WebSockets.

The WebSocket request sent by my application code to my own endpoint (handled by my Express server) stalls, meaning it never resolves or connects: CleanShot 2024-08-08 at 13 12 45@2x

I don't have any pointers on how to solve this issue. In the issue #85, you link a Fastify example but I'm not quite sure how it applies to my code. Even though you are stating in that issue that using "httpDevServer is a hack", I think it's an extremely useful and simple API and I much prefer it to the alternative handler API. I have tried the handler API too by the way, and it has the same behavior regarding WebSockets.

Generally speaking, what's the mental model to have with vavite? Is it that Vite will first try to resolve routes in the Vite way, and when that fails, it forwards it to the user-provided server - or is it the other way around, where the user-provided server can "steal" URLs from Vite by handling them first? In any case, why is it that my /ws-brooo route (called it like that so that it doesn't clash) is handled by Vite, and not by my internal server? Why does it "interest" Vite's dev server?

Vanilagy avatar Aug 08 '24 11:08 Vanilagy

Hi,

Vavite basically does two things:

First, it automates multiple build steps: Usually you need to run Vite twice to build your server bundle and client bundle. Vavite can automate it for you via the buildSteps option. This one is rather trivial, it basically wraps Vite in its own CLI that runs it more than once. It will become obsolete once Vite's Environment API which is currently in beta becomes stable.

Second, it allows you to use Vite's dev server to transpile your server code during development. Vite's own SSR guide recommends using Vite in "middleware mode". If we followed it, we would use vite.createServer with { server: { middlewareMode: true } } to create a Vite development middleware, then create a Node server with, e.g. Express, and add the Vite middleware to it. Then we would use viteDevServer.ssrLoadModule to load our server code.

This flow poses a chicken-and-egg problem if we want to use TypeScript or JSX: We need Vite's dev server to compile our server code but we need the server code to create the Vite middleware. We could use a separate tool like ts-node but to me, it doesn't make sense to use a separate tool when Vite is already there. This approach also requires us to give up on Vite's CLI and handle all the options ourselves. That's not ideal.

Vavite solves it by adding the user handler to Vite's dev server as a middleware. That's why httpDevServer is a hack: It tries to shoehorn a full server into a middleware using Proxy and such. With the handlerEntry option, you just export a handler. You can still give an additional serverEntry to control how you create the server, import the handler from your handler entry, and add it as a middleware (but it will only work on production, not with Vite's dev server).

Is it that Vite will first try to resolve routes in the Vite way, and when that fails, it forwards it to the user-provided server - or is it the other way around, where the user-provided server can "steal" URLs from Vite by handling them first?

It can do either depending on the value of the serveClientAssetsInDev option. When set to true, Vite will handle the request before the user, when false, the user will have a chance first and Vite will kick in only if the user doesn't handle send a reply.

You may think that the name serveClientAssetsInDev is misleading since it will still serve the assets (transpiled on-the-fly by Vite) but in reality most server frameworks (e.g. Fastify) will always return a response so Vite's dev server will never get a chance. But it is possible with Express or plain node:http to defer to Vite. So serveClientAssetsInDev: false is really only useful for pure server-side applications.

Letting Vite do its thing before your server is also more consistent with production behavior: Static assets are usually handled before other routes. In fact many people will put something like a CDN or Nginx in front of their Node server and serve static assets from there so those requests won't even make it to Node. Many deployment platforms like Vercel, Netlify, and Cloudflare behave similarly (but usually it is possible to work around that with some configuration).


As for the WebSocket problem, can you provide a simple reproduction so I can have a look at it?

cyco130 avatar Aug 11 '24 02:08 cyco130

Thanks for the lovely answer! I always thought the "chicken-and-egg problem" you mentioned was a big "gotcha" in Vite; the middleware mode seems great at first until you realize you now need to take care of running TypeScript yourself. Recently, I've been using vite-node for this. This works fine but I've been looking towards Vavite for a more unified approach.

As for the WebSockets problem, here's a repo including the repro: https://github.com/Vanilagy/vavite-express-websocket-issue

I originally intended to StackBlitz it, but using the console to inspect the network request was extremely finnicky and didn't really work, so I opted instead for a simple, barebones repo. Setup is really easy, it's just npm i followed by npm run dev. Navigate to /index.html, you'll see a link to a test route that is served by the user-provided server and not by Vite directly. You'll also see a button that opens a socket - open the console first, then click that button. I never see the socket fully opening its connection on my end, it simply remains in a pending state. Not sure if it's relevant, but I'm on MacOS 14.6 running the latest Chrome.

An interesting thing I noticed was that the / route broke as soon as I installed the Vavite plugin. As in, now I have to navigate to /index.html explicitly. What gives? Perhaps Vite implements these "directory paths" (with no file) as a fallback handler, which only kicks in if all other handlers have been exhausted, and since the user server is in the way, it never hits it?

What was also interesting was that the dev server completely stops working if I don't import httpDevServer. My expectation was that, when not importing httpDevServer, I would just get the default Vite behavior. Is this behavior caused by you doing specific server initialization logic once the vavite/http-dev-server import is resolved?

Vanilagy avatar Aug 11 '24 11:08 Vanilagy

I'll have a look at the repro, thank you.

About Vite rewriting / to /index.html, just add appType: "custom" to your Vite config. Vite operates in "SPA mode" by default.

cyco130 avatar Aug 12 '24 12:08 cyco130

That's good to know, thank you!

Vanilagy avatar Aug 12 '24 14:08 Vanilagy

Any updates?

Vanilagy avatar Aug 20 '24 09:08 Vanilagy

I added a WebSocket example. The code has a lot of comments to explain how everything works (and alternatives).

I'm not very familiar with the express-ws package so I used plain ws but I suppose express-ws could be made to work in a similar way.

cyco130 avatar Sep 07 '24 15:09 cyco130

Hey, thank you for the example. I'm writing here so you know your work is appreciated and acknowledged. I'm not working on my Vavite-based project anymore at the moment, which is I why I haven't replied. I just know how I (as a library author) am bothered by inactive GitHub issues, so I just wanted to set things straight. <3

This is probably not a big deal to you at all, it's just that this issue has been in the back of my mind for a long time as "incomplete".

Vanilagy avatar Aug 08 '25 19:08 Vanilagy