RFC: WebSocket Upgrades in Route Handlers #95514
Replies: 3 comments 1 reply
-
|
I think a separate websocket.ts file would make more sense, especially for Vercel Functions where you normally think of route handlers as short-lived request/response code. WebSockets have a different mental model since the connection lives longer and can have per-connection state. |
Beta Was this translation helpful? Give feedback.
-
|
Good RFC. A few implementation questions worth considering before moving to experimental. On the upgrade handshake: the proposed NextResponse.upgrade() approach hides what happens at the HTTP level. The RFC should specify whether Next.js will handle the 101 Switching Protocols response and the Upgrade: websocket header automatically, or whether the handler author is responsible. Vercel infrastructure currently terminates WebSocket connections at the edge, so the behavior on self-hosted deployments and on Vercel should be documented separately. On authentication: the example shows calling isAuthorized(request) before the upgrade. Once the handshake succeeds, the original request object is no longer accessible inside open/message/close. The RFC should show how to pass auth context (e.g. user id from JWT) into the peer lifecycle callbacks, otherwise developers will re-parse cookies on every message. On the peer abstraction: peer.send() accepting an object and auto-serializing to JSON is a useful default, but the RFC needs to specify what happens when a non-serializable value is passed and whether binary frames are supported. On deployment: WebSocket connections on serverless functions need to be backed by a persistent compute layer. The RFC should clarify how long a connection can live, what happens when the function instance is recycled, and whether this targets edge or Node.js runtime only. The NextResponse.upgrade() API surface looks clean. The main gap is specifying the operational constraints so developers can decide whether to use this or an external WebSocket service. |
Beta Was this translation helpful? Give feedback.
-
|
The CrossWS approach here is a solid foundation. A few thoughts on the draft. The handler-executes-once-per-handshake model is the right call. It keeps the authentication and request-processing pattern consistent with regular route handlers, so developers can apply cookies, headers, and early returns without learning a separate API. One thing worth clarifying in the RFC is the lifecycle for cookies set on the upgrade response. Standard WebSocket handshakes do send a Set-Cookie in the 101 response, but many proxies and clients strip or ignore it. The example sets cookies.set on the upgrade response, which raises the question of whether those cookies will reliably reach the browser. Calling this out explicitly in the spec would save a lot of confusion for developers who discover the cookies are not persisting in production. On the peer ID in the x-connection-id header: the 101 response headers are not accessible to the browser via the WebSocket API (the browser WebSocket constructor gives no access to handshake response headers). If the connection ID is needed by the client, the pattern would be to send it as the first message in the open hook rather than as a header. Worth noting in the docs or examples. The scope limitation around adapters is important. The adapter section should make clear that NextResponse.upgrade() on platforms that do not support the raw upgrade primitive will need to fall back gracefully rather than hanging. A clear error at dev time would help. The experimental flag makes sense here. The main compatibility risk is Vercel deployment, since their edge network typically terminates WebSocket connections at the load balancer. The RFC should state whether NextResponse.upgrade() is expected to work on Vercel or only on self-hosted Node.js. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
NextResponse.upgrade()Summary
This RFC proposes first-class WebSocket support in App Router Route Handlers:
The handler executes exactly once per handshake. Returning an ordinary
Responserejects the upgrade with that response. ReturningNextResponse.upgrade()accepts the connection after the handler completes.Every accepted connection receives a real CrossWS
Peercreated by Next.js’s bundled CrossWS Node adapter. Applications use the CrossWS peer, message, and lifecycle-hook APIs rather than the underlyingws.WebSocketAPI.Self-hosted Next.js handles this through Node.js’s HTTP
upgradeevent. Adapters support it by forwarding the raw upgrade primitives to the generated App Route upgrade handler.Motivation
Next.js currently has no first-class way for an App Route Handler to accept an incoming WebSocket connection.
Users must instead:
upgradelistener.route.ts.A first-class API should satisfy these requirements:
Requestbefore accepting.proxy.ts, rewrites, instrumentation, and error handling remain part of Next.js.Goals
route.ts.PeerandMessageAPIs.next dev,next start, standalone output, and compatible Adapters.Non-goals
proxy.ts.wsobject.Proposed API
The hooks object can implement any of these lifecycle callbacks:
open(peer)after the handshake succeeds.message(peer, message)when a message arrives.close(peer, details)when the connection closes.error(peer, error)for transport errors.CrossWS peers provide
send(),close(),terminate(),subscribe(),unsubscribe(), andpublish(). Messages provide conversion helpers includingtext(),json(),uint8Array(), andarrayBuffer().Plain objects passed to
peer.send()orpeer.publish()are serialized as JSON.Next.js pins and bundles CrossWS 0.4.4 with its declarations, so applications do not need to install
crossws,ws, or their type packages.peer.websocketremains CrossWS’s compatibility proxy; Next.js does not expose or guarantee rawwsevent-emitter, ping, pong, or constructor-identity behavior.peer.requestuses CrossWS’s Node Request-compatible view. The complete incoming request remains available before acceptance through the Route Handler’sRequestargument.CrossWS Integration
Each generated App Route module owns one shared CrossWS Node adapter. Connections handled by the same route therefore participate in CrossWS peer tracking, subscriptions, and publication.
CrossWS namespaces default to the resolved request pathname. Dynamic route instances with different pathnames are isolated by default.
The initial implementation uses fixed transport settings:
Per-handshake transport options are not exposed because creating separate server instances would fragment route-wide peer tracking and publish/subscribe behavior.
Response Semantics
Conditional acceptance
For a WebSocket handshake:
upgradeevent.403.Browser WebSocket APIs report a failed connection and generally do not expose the HTTP status or body. Node.js WebSocket clients can observe the early HTTP response through their client-specific APIs.
Successful acceptance
When the handler returns an upgrade response:
open(peer).No lifecycle hook runs before the handler returns the upgrade response and the handshake succeeds.
Headers and cookies
Mutations made before returning the response are included in the handshake. Mutations made after returning have no effect.
Next.js controls these protocol-critical headers:
ConnectionUpgradeSec-WebSocket-AcceptSec-WebSocket-ExtensionsSec-WebSocket-ProtocolContent-LengthTransfer-EncodingSubprotocol selection is not exposed in the initial API.
Hook errors
Lifecycle hooks run after the handshake. Their errors cannot be converted into an HTTP response.
Synchronous exceptions and rejected promises from
open,message, orerror:1011.Errors from
closeare reported without attempting to close the peer again.Fallible initialization should happen before returning:
Next.js observes hook promises for rejection but does not include them in the Route Handler request lifetime.
Self-hosted Node.js Behavior
Node.js handles upgrade requests separately from ordinary requests. A valid WebSocket handshake emits
upgradeinstead ofrequest.The Route Handler therefore executes once:
next devNext.js already handles development WebSockets such as HMR.
Upgrade routing must preserve this order:
Application WebSocket paths must not interfere with
/_next/hmror other internal channels.next startThe production Node.js server:
upgradeevent.proxy.ts, redirects, and rewrites.GEThandler.The current router closes upgrade requests that resolve to application routes. This branch would instead invoke the matching App Route.
Standalone output
Standalone output includes the same router and Node.js upgrade integration.
The output trace must include Next.js’s bundled CrossWS Node adapter and its required supporting files.
Custom servers
Custom servers using Next.js’s upgrade handling receive the same behavior.
Custom servers that bypass Next.js’s upgrade handler remain responsible for forwarding:
into Next.js.
Existing unrelated upgrade listeners continue receiving paths that Next.js does not handle.
Ordinary HTTP requests
If a handler returns
NextResponse.upgrade()from the normal HTTP request path, no upgrade socket is available.Next.js returns:
Shutdown
Next.js tracks accepted CrossWS peers by their canonical App Route bundle path.
During graceful shutdown:
1001where possible.When a route is invalidated in development, its peers receive close code
1012. Next.js waits up to five seconds before terminating remaining peers. New connections use the updated route module, while internal HMR WebSockets remain unaffected.Adapter Behavior
Build-time Adapters API
The build-time
NextAdapterinterface andAdapterOutputshape do not need to change.WebSocket acceptance is a possible runtime result of an App Route, just like an ordinary response.
Adapters continue receiving the existing output:
No capability field is proposed. Conditional calls to
NextResponse.upgrade()cannot be reliably identified at build time.Full-server Adapters
Adapters deploying the complete Next.js Node.js server receive WebSocket support through the server’s upgrade handling.
The Adapter must ensure that its deployed environment allows the Node.js HTTP server to receive raw upgrade events and maintain long-lived TCP connections.
No App Route-specific integration is required.
Individual App Route outputs
The existing Node.js App Route entrypoint handles ordinary requests:
A Node.js upgrade event provides:
It does not provide a
ServerResponse.Generated Node.js App Route outputs therefore gain an additive, Adapter-facing entrypoint:
An Adapter that invokes individual route outputs must forward the raw upgrade event to this entrypoint.
The upgrade handler:
Request.Unsupported Adapters
An Adapter that knows it cannot provide these capabilities must disable the feature through its existing
modifyConfighook:modifyConfigruns after the application configuration is resolved, so the Adapter override takes precedence even if the application enables the experimental flag. No WebSocket capability field is added toNextAdapterorAdapterOutput.When the Adapter disables the flag, calling
NextResponse.upgrade()throws the normal flag-disabled configuration error in development and production. The error occurs when the Route Handler executes that code path; disabling the flag does not make the application fail at startup solely because WebSocket Route Handler code exists.If a generated upgrade handler is nevertheless invoked without the required raw Node.js primitives or persistent socket support, it produces:
Next.js also logs a descriptive runtime error. This is a defensive fallback rather than the Adapter’s primary feature-disabling mechanism.
Adapter behavior matrix
next devnext startupgradeHandlerexperimental.webSocketRouteHandlersthroughmodifyConfig; use throws the flag-disabled errorRouting and
proxy.tsUpgrade requests use the existing routing order:
proxy.tsproxy.tscan allow, rewrite, redirect, or reject a handshake.NextResponse.upgrade()is only valid in App Route Handlers and is not supported fromproxy.ts.If
proxy.tsreturns an ordinary response, that response rejects the handshake.Internal Next.js WebSocket paths retain priority over application routes.
Caching and Static Generation
Upgrade responses cannot be cached or prerendered.
Calling
NextResponse.upgrade()marks the route dynamic and sets its effective revalidation behavior to zero. Calling it conditionally during a revalidation will be a runtime error.The following produce an error if an upgrade response is encountered during static generation:
output: 'export'dynamic = 'force-static'The entire Route Handler is treated as dynamic, even if some branches return ordinary cacheable responses.
Security Considerations
Applications should validate:
OriginNext.js does not automatically enforce same-origin WebSockets in production.
The response cannot override protocol-critical handshake headers.
The internal upgrade marker must use private framework state rather than a forgeable incoming header.
Per-message compression defaults to disabled. Incoming payload size is bounded by default.
Upgrade Response Representation
Node.js’s
Responseimplementation does not permit status code 101.NextResponse.upgrade()must therefore return a framework response carrying:The object:
Generic reconstruction may lose the upgrade metadata:
Next.js must inspect the result before cloning, caching, serializing, or reconstructing it.
Detailed Node.js Execution
Conceptually, each generated App Route module owns one shared adapter:
An internal CrossWS
upgradehook transfers the returned response headers, cookies, and private per-connection metadata into the handshake. It is not exposed publicly because the Route HandlerGETremains the sole pre-acceptance authentication and response phase.NextResponse.upgrade()stores the hooks directly on the returned response. No hook registry or second Route Handler execution is required.Trade-offs
Benefits
NextResponseAPIs.proxy.tsbehavior is preserved.Considerations
wsevent-emitter, ping, and pong APIs are not part of the supported contract.peer.websocketis CrossWS’s compatibility proxy and may vary across runtimes.Alternatives Considered
Separate route export
This makes conditional ordinary responses, cookies, and response headers less natural. It also introduces a method-like export that is not an HTTP method.
request.upgrade()This implies immediate acceptance and makes the point at which the handshake is committed less clear.
Dedicated
websocket.tsconventionA separate file would make upgrade routes easier to identify statically, but it would split related HTTP authentication and WebSocket behavior across files.
Custom server only
Users can register
server.on('upgrade'), but that bypasses Route Handler routing and does not provide consistent Adapter integration.Expose
ws.WebSocketdirectlyThis closely matches the underlying Node.js transport, but makes a low-level, runtime-specific API public and provides no common peer, message, namespace, or publication abstraction.
Expose the CrossWS
upgradehookCrossWS has a pre-acceptance
upgradehook. Exposing it would create a second authentication and response phase after the App RouteGEThandler.The Route Handler already provides request inspection, conditional responses, cookies, and headers, so only the four connection lifecycle hooks are public.
Create one CrossWS adapter per connection
This would make per-handshake payload and compression settings straightforward, but subscriptions, publication, and peer tracking would not work across route connections. One shared adapter per generated route is preferred.
Application-installed CrossWS
Next.js could require applications to install CrossWS and supply their own adapter instance.
This introduces version compatibility, output tracing, and dependency-resolution concerns. Next.js’s pinned CrossWS build provides a deterministic peer and message contract without an application dependency.
Rollout
experimental.webSocketRouteHandlers.next dev,next start, and standalone support.upgradeHandlerexport.modifyConfig.Test Plan
next devandnext start.open,message,close, anderrorhooks.close()andterminate().1011.proxy.ts.upgradeHandler.experimental.webSocketRouteHandlersoff throughmodifyConfigin development and production.501response when an upgrade handler lacks raw Node.js primitives.References
upgradeeventBeta Was this translation helpful? Give feedback.
All reactions