-
Notifications
You must be signed in to change notification settings - Fork 27.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Support WebSocket API routes, Upgrade requests #58704
base: canary
Are you sure you want to change the base?
feat: Support WebSocket API routes, Upgrade requests #58704
Conversation
|
||
const { matchedOutput, parsedUrl } = await resolveRoutes({ | ||
req, | ||
res: socket as any, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
зачем удалил?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary, the regular request handling event can handle upgrade requests.
546ebdc
to
6bbe950
Compare
Enables route handlers to receive and act on `Connection: Upgrade` requests, such as WebSockets, when using the Node runtime. To enable this, the base http server has the `on('upgrade')` handler removed. In this author's opinion, that handler is an anti-pattern as it makes it much more difficult to handle middleware and other request lifecycle behavior. By passing the raw request to the route handler and implementing a `NextResponse.upgrade()` response value to opt out of additional processing that would write to the socket, the route handler can handle an upgrade request itself. Fixes vercel#58698 (feature request) Fixes vercel#56368 (caused by next-ws / websocket middleware)
6bbe950
to
89bc37c
Compare
When the request is upgraded as part of a request listener, one of three things must happen in order to prevent Node.js from closing the connection: 1. Disable request timeouts. 2. The request handler may mark the `OutgoingMessage` as having the response headers sent before the timeout expires. This happens as part of `flushHeaders()` calling `Socket#_send()` in `node.js/lib/_http_outgoing.js`[^1]. 3. Add an event listener on the `http.Server` for `clientError` events to handle the HTTP request timeout error. In `node.js/lib/_http_server.js`[^2], the presence of an event listener bypasses error handling. We rule out the first two options as: 1. This would re-introduce Slowloris and other vulnerabilities that are the reason these timeouts were added. 2. The most popular Websocket library, `ws`, writes to the raw socket and does not use `OutgoingMessage` methods which would mark the headers as sent. This leaves the third option, and we implement a workaround as seen in [koa-easy-ws#36](b3nsn0w/koa-easy-ws#36). [^1]: https://github.com/nodejs/node/blob/v20.10.0/lib/_http_outgoing.js#L378 [^2]: https://github.com/nodejs/node/blob/a2206640f366cc145b17a2ece66d36bfa75720ee/lib/_http_server.js#L883
Looks like I'm dealing with the same exact server timeout issue that was addressed by your recent commit. I must say great job on the documentation and research! Overriding the Hopefully we get some more eyes on this feature and it gets merged soon! |
can you expand on that? |
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
@feedthejim - sure.
Upgrade handlers don't match user routing expectationsTo participate in advanced routing behavior, one has to reimplement it: There are bugs with this approach: it doesn't handle Upgrade handlers need to re-implement middlewareTo implement It doesn't make sense to me, but you can try this yourself below. So if a Next.js user installs a global middleware to perform authn/authz, then installs import * as http from 'node:http';
import assert, { deepStrictEqual } from 'node:assert';
import { WebSocket, WebSocketServer } from 'ws';
let logOutput: string[] = [];
let log = (message: string) => {
logOutput.push(message);
console.log(message);
}
let globalMiddleware = (req, res) => {
// Imagine this is global auth middleware!
log('global-middleware');
};
let pathMiddleware = (req, res) => {
log('path-middleware');
};
const server = http.createServer((req, res) => {
globalMiddleware(req, res);
if (req.url === '/middleware') {
pathMiddleware(req, res);
}
log('request');
res.statusCode = 200;
res.end('Hello World');
});
server.on('upgrade', (req, socket, head) => {
log('upgrade');
socket.end();
// Must manually implement the response, write to the socket, etc. Middleware is ignored.
});
const port = Math.floor(Math.random() * 20_000 + 20_000);
console.log(`Starting server on port ${port}`);
await new Promise<void>((resolve) => {
server.listen(port, () => {
resolve();
});
});
console.log(`Server started on port ${port}`);
console.log('\n\nTesting /');
logOutput = [];
await fetch(`http://localhost:${port}/`);
assert.deepStrictEqual(logOutput, [ 'global-middleware', 'request' ]);
console.log('\n\nTesting /middleware');
logOutput = [];
await fetch(`http://localhost:${port}/middleware`);
assert.deepStrictEqual(logOutput, [ 'global-middleware', 'path-middleware', 'request', ]);
logOutput = [];
console.log('\n\nTesting /websocket');
await new Promise<void>((resolve) => {
const ws = new WebSocket(`ws://localhost:${port}/websocket`);
ws.on('error', () => {
// Ignore the error
resolve();
});
});
assert.deepStrictEqual(logOutput, [ 'upgrade' ]);
server.close(); The alternativeImplement upgrades as part of the request handler and main router. We get parameter parsing "for free", middleware compliance "for free", niceties in Next such as |
Any progress on this? |
@feedthejim curious if you've had a chance to look more deeply in this? |
Could you come with example please how to implement upgrades without on.upgrade? |
@bladerunner2020 upgrades involve "hijacking" the socket (in Go terminology), wherein the request handler for the websocket route takes full control of the duplex socket. The To implement upgrades in a request, we must do a few things: First, ensure that the Node runtime does not close or drop our socket. That's implemented here:
Second, we add a method on the request object to hijack or "upgrade" the request from within an ordinary request handler: next.js/packages/next/src/server/web/spec-extension/request.ts Lines 119 to 134 in 486b362
Third, from within a regular request handler, we call next.js/test/e2e/app-dir/app-routes/app/api/websocket/route.ts Lines 6 to 10 in 486b362
The Websocket library parses the request headers and completes the handshake, writing the |
@ztanner @ijjk Any chance this could get looked at? #56368 Would also get addressed by this PR. For anyone using WebSockets (popular with LLM projects), this is potentially impeding progress. @AaronFriel There appear to be a few conflicts on this PR. Is this still a viable solution in your opinion? |
@Chidocs I think it's still viable, and I expect I'll update this is this is something Vercel will review. @ijjk @feedthejim et al., this is one of the highest upvoted PRs on the repo! I'd love to get a review. |
Enables route handlers to receive and act on
Connection: Upgrade
requests, such as WebSockets, when using the Node runtime. To enable this, the base http server has theon('upgrade')
handler removed. In this author's opinion, that handler is an anti-pattern as it makes it much more difficult to handle middleware and other request lifecycle behavior.By passing the raw request to the route handler and implementing a
NextResponse.upgrade()
response value to opt out of additional processing that would write to the socket, the route handler can handle an upgrade request itself.Fixes #58698 (feature request)
Fixes #56368 (caused by next-ws / websocket middleware)