How the wsRouter Manages WebSocket Connections Separately from HTTP in code-server

The wsRouter in code-server intercepts the HTTP server's upgrade event to route WebSocket handshakes through a dedicated Express instance, keeping TCP socket handling isolated from standard HTTP middleware.

The coder/code-server repository solves the challenge of handling WebSocket upgrades alongside traditional HTTP traffic by maintaining two distinct Express applications. This architecture ensures that WebSocket connections are processed through a specialized middleware stack while preventing accidental interference with regular HTTP request handling.

The Dual Router Architecture

code-server creates two separate Express applications in src/node/app.ts to isolate traffic types:

  • app.router – Processes standard HTTP methods (GET, POST, PUT, DELETE)
  • app.wsRouter – Handles exclusively WebSocket upgrade requests

This separation prevents middleware designed for HTTP requests from accidentally processing WebSocket handshake packets. The HTTP server instance is shared between both routers, but traffic is diverted at the TCP level before reaching Express's routing layer.

Intercepting the WebSocket Upgrade

The core mechanism resides in src/node/wsRouter.ts within the handleUpgrade function. This listener attaches to the Node.js HTTP server's "upgrade" event, which fires when a client sends an Upgrade: websocket header.

Creating the InternalWebsocketRequest

When an upgrade event fires, handleUpgrade constructs a mock Express request object enriched with raw socket data:

// src/node/wsRouter.ts
export const handleUpgrade = (app: express.Express, server: http.Server): void => {
  server.on("upgrade", (req, socket, head) => {
    socket.pause()

    const wreq = req as InternalWebsocketRequest
    wreq.ws = socket               // expose raw TCP socket
    wreq.head = head               // initial data frame
    wreq._ws_handled = false       // route matching flag

    // Dispatch to Express router
    ;(app as any).handle(
      wreq,
      new http.ServerResponse(wreq),
      () => {
        if (!wreq._ws_handled) {
          socket.end("HTTP/1.1 404 Not Found\r\n\r\n")
        }
      },
    )
  })
}

The function pauses the socket immediately to prevent data loss, then invokes Express's internal handle method with a modified http.ServerResponse. This allows the WebSocket router to apply Express routing logic to the raw upgrade request.

The 404 Fallback Mechanism

If no WebSocket route matches the request URL, the final callback executes and checks wreq._ws_handled. When this flag remains false, the server sends a raw HTTP 404 response and terminates the connection, ensuring that unmatched upgrade requests fail gracefully rather than hanging indefinitely.

The WebSocket Router Implementation

The WebsocketRouter class in src/node/wsRouter.ts provides a ws() method for registering WebSocket endpoints. Since WebSocket handshakes begin as HTTP GET requests, the implementation leverages Express's existing router.get() infrastructure.

Wrapping Handlers with _ws_handled Flag

Each registered handler is wrapped to set the _ws_handled flag before execution:

// src/node/wsRouter.ts
export class WebsocketRouter {
  public readonly router = express.Router()

  public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
    this.router.get(
      route,
      ...handlers.map((handler) => {
        const wrapped: express.Handler = (req, res, next) => {
          ;(req as InternalWebsocketRequest)._ws_handled = true
          return handler(req as WebsocketRequest, res, next)
        }
        return wrapped
      }),
    )
  }
}

Setting _ws_handled = true signals to handleUpgrade that the connection has been claimed by a valid handler, suppressing the 404 fallback response. This mechanism allows multiple middleware functions to chain together while ensuring the connection is marked as handled before reaching the final WebSocket logic.

Wiring Everything Together

In src/node/app.ts, the createApp function instantiates both routers and binds the upgrade interceptor:

// src/node/app.ts
const router = express()           // Standard HTTP routes
const wsRouter = express()         // WebSocket-only routes
handleUpgrade(wsRouter, server)    // Attach upgrade listener

return { router, wsRouter, server, ... }

The separation is enforced at the route mounting level in src/node/routes/index.ts:

// src/node/routes/index.ts
app.router.use("/", domainProxy.router)           // HTTP traffic only
app.wsRouter.use("/", domainProxy.wsRouter.router) // WebSocket traffic only

app.router.use(vscode.router)                    // VS Code HTTP routes
app.wsRouter.use(vscode.wsRouter.router)         // VS Code WebSocket routes

This mounting strategy guarantees that HTTP middleware like body parsers or cookie handlers never process WebSocket upgrade requests, while WebSocket-specific authentication and proxy logic remains isolated from standard REST endpoints.

Real-World Usage: VS Code WebSocket Proxy

The VS Code integration in src/node/routes/vscode.ts demonstrates practical application of this architecture:

// src/node/routes/vscode.ts
export const wsRouter = WsRouter()

wsRouter.ws(
  /.*/,
  ensureOrigin,
  ensureAuthenticated,
  ensureVSCodeLoaded,
  async (req: WebsocketRequest) => {
    const wrapped = await socketProxyProvider.createProxy(req.ws)
    vscodeServer!.handleUpgrade(req, wrapped as net.Socket)
    req.ws.resume()          // Resume frame processing
  },
)

This route captures all WebSocket connections under the VS Code path, runs authentication middleware, then proxies the raw socket to the underlying VS Code server instance. The req.ws.resume() call is critical—it restarts data flow after the socket was paused during the initial upgrade interception.

Summary

  • handleUpgrade in src/node/wsRouter.ts intercepts HTTP upgrade events and converts raw TCP sockets into Express-compatible request objects.
  • WebsocketRouter.ws registers handlers that mark requests with _ws_handled to prevent 404 fallbacks.
  • Dual Express instances in src/node/app.ts keep WebSocket and HTTP middleware stacks completely isolated.
  • Route mounting in src/node/routes/index.ts ensures traffic types are dispatched to their respective routers without cross-contamination.
  • The architecture allows code-server to proxy WebSocket connections to VS Code's internal server while maintaining separate security and routing logic for HTTP requests.

Frequently Asked Questions

How does code-server prevent HTTP middleware from handling WebSocket upgrades?

code-server creates two independent Express applications—one for HTTP and one for WebSockets—then binds the handleUpgrade interceptor exclusively to the WebSocket instance. This ensures that middleware mounted on the standard HTTP router never sees WebSocket upgrade requests, as they are diverted to app.wsRouter at the TCP level before reaching Express routing logic.

What happens if no WebSocket route matches the upgrade request?

If no registered handler sets req._ws_handled = true, the callback in handleUpgrade sends a raw HTTP/1.1 404 Not Found response and terminates the socket connection. This prevents unhandled upgrade requests from leaving connections in a hanging state and provides clear feedback to clients attempting invalid WebSocket endpoints.

Why does the wsRouter use Express's router.get() instead of a custom method?

WebSocket handshakes technically begin as standard HTTP GET requests containing the Upgrade: websocket header. By wrapping handlers inside router.get(), WebsocketRouter leverages Express's existing path matching and middleware chaining capabilities while enriching the request object with the raw TCP socket and head buffer needed for WebSocket communication.

Can standard Express middleware be used with WebSocket routes in code-server?

Yes, but only middleware specifically designed to handle the enriched WebsocketRequest interface. Standard middleware expecting req.body or typical HTTP properties may fail because WebSocket upgrade requests contain minimal HTTP headers and no body content. The architecture allows authentication and origin-checking middleware (like ensureAuthenticated) to run before the socket is consumed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →