next.js: Next 13 /_next/webpack-hmr pending indefinitely with custom server
Verify canary release
- I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: win32
Arch: x64
Version: Windows 10 Pro
Binaries:
Node: 18.15.0
npm: N/A
Yarn: N/A
pnpm: N/A
Relevant packages:
next: 13.4.5-canary.0
eslint-config-next: 13.4.4
react: 18.2.0
react-dom: 18.2.0
typescript: 5.0.4
Which area(s) of Next.js are affected? (leave empty if unsure)
App directory (appDir: true)
Link to the code that reproduces this issue or a replay of the bug
https://github.com/luca-vogels/luca-vogels
To Reproduce
import http from "http";
import express, { NextFunction, Request, Response } from "express";
import next from "next";
const dev = true;
const HTTP_BIND = "0.0.0.0";
const HTTP_PORT = 80;
const nextApp = next({ dev });
const nextHandler = nextApp.getRequestHandler();
const nextUpgradeHandler = nextApp.getUpgradeHandler();
nextApp.prepare().then(async () => {
const app = express();
const server = http.createServer(app);
// all frontend routes
app.all('*', (req: Request | any, res: Response) => {
return nextHandler(req, res);
});
// all frontend routes (upgrade)
server.on("upgrade", (req: Request, socket, head) => {
nextUpgradeHandler(req, socket, head);
});
// start server
server.listen(HTTP_PORT, HTTP_BIND, function(){
console.log("Server running at "+HTTP_BIND+":"+HTTP_PORT+" in "+(dev ? "development" : "production")+" mode");
});
});
Describe the Bug
Since Next 13, when using with a custom server (e.g. express), the route /_next/webpack-hmr
gets no longer served (pending indefinitely) which breaks the hot-refresh functionality!
Next 12 worked flawlessly and the nextApp.getUpgradeHandler()
wasn’t even needed.
Working example with Next 12:
import express, { NextFunction, Request, Response } from "express";
import next from "next";
const dev = true;
const HTTP_BIND = "0.0.0.0";
const HTTP_PORT = 80;
const nextApp = next({ dev });
const nextHandler = nextApp.getRequestHandler();
nextApp.prepare().then(async () => {
const app = express();
// all frontend routes
app.all('*', (req: Request | any, res: Response) => {
return nextHandler(req, res);
});
// start server
app.listen(HTTP_PORT, HTTP_BIND, function(){
console.log("Server running at "+HTTP_BIND+":"+HTTP_PORT+" in "+(dev ? "development" : "production")+" mode");
});
});
Expected Behavior
Route /_next/webpack-hmr
gets served again by the getRequestHandler()
provided by next
, so hot-refreshing works as expected.
Worked in Next 12 as well.
Which browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
Node (custom server)
About this issue
- Original URL
- State: open
- Created a year ago
- Reactions: 31
- Comments: 31 (6 by maintainers)
still exists next@13.4.10
Facing same issue in next 14.0.2. Webpack-hmr just never finishes.
I’m experiencing the same issue with the Payload Custom Server Example which uses the Next.js App Router for its demonstration. It worked flawlessly with the Pages Router, but since upgrading to App Router, HMR no longer works. The app compiles in real-time but the browser requires a refresh to see the changes.
The issue was introduced with https://github.com/vercel/next.js/pull/49805/files
It adds an extra proxy layer but does not proxy WebSockets.
next-dev-server
magically adds a WebSocket handler to the request’s server such that if you use custom server (express
orhttp.createServer
) and passreq
to the next.js handler (returned by.getRequestHandler()
) you will have a WebSocket handler on your custom server.However, since v13.4.3-canary.3 next.js forces creation of
render-server
instead, which is similar to a custom server in that it callsnext({ ... })
and forwards requests to it, while thenext({ ... })
you called simply proxies requests torender-server
(render-server-standalone.ts
). Butrender-server
is it’s ownHttpServer
. In this casenext-dev-server
adds WebSocket handler onto theHttpServer
ofrender-server
. And there is nothing that would forward WebSocket from your custom server to therender-server
to even have a chance to be forwarded tonext-dev-server
.render-server-standalone.ts
should have had it’s own version of whatDevServer.setupWebSocketHandler
does.I take that back. The upgrade handler gets added automatically on the first request as before. However, I am adding my own websocket to a custom server (needs to be on the same port). I only handle upgrade requests that match a certain path, much like the HMR upgrade handler that only handles requests for
_next/webpack-hmr
.But, commit 1398de9977b89c9c4717d26e213b52bc63ccbe7e introduced in 13.4.13 made it so the socket gets closed when not handled by HMR (before, it worked fine to have the HMR upgrade handler added to the server alongside my own). This is bad because there is now currently no clean way I can see to add your own socket to the routing server. I think the plan is to support upgrade requests in app/pages routes, but like I said the socket is now being closed instead of being allowed to continue.
I’m working around it for now with the following…
Sorry, that won’t work either. It fixed my own websocket, but HMR stopped working. I had assumed I could call the upgradeHandler in the
'upgrade'
event listener, but that’s apparently not equivalent. ~If I want both, I’ll have to downgrade to 13.4.12.~ 13.4.19 seems to have a HMR fix for custom server + app router (which I think is OP’s issue), but I’m kind of stuck.Fixing the user websocket regression would be a big bonus for me.
@ijjk I think all that would be needed to fix any user websockets would be to remove this line.
Side note: I think these are all related to HMR with a custom server + app router: #51162, #51028, #50881, #50400
@luca-vogels
getRequestHandler()
andgetUpgradeHandler()
don’t do what you think they do if you haveappDir
. They actually point to completely different servers, with the latter being a no-op server, so usinggetUpgradeHandler()
is futile. The issue is much deeper.Instead of
is it
WebSocket was just completely overlooked in this extra proxy layer. Someone with a deeper understanding of how and why all of this was implemented this way should have a look and come up with a proper fix.
Here is roughly what is missing to support
getUpgradeHandler()
:If you find yourself in a situation where you can not downgrade (due to other fixes in 13.4.3+) and your local setup is barely usable (without HMR) then:
https://github.com/ds300/patch-package
Disclaimer: The maintenance of local patches is on you. If you can wait while on 13.4.2, better do so.
patches/next+13.4.6.patch
(can be13.4.3
to13.4.6
):13.4.19 does indeed seem to be working, but I think the custom server example needs to be updated to include
getUpgradeHandler
…Downgrade next to 13.3 works for me 🤷 (needed to add appDir: true in config)
I would also like to add I’m encountering this issue. I’m running next.js 13.5.4, with a REST server hosted on Flask. The Webpack-hmr request remains infinitely pending in my browser tools. Note, this does not have any impact on my hot-reload.
I can confirm this.
No hacks necessary now?