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

> Discover how code-server's wsRouter separates WebSocket connections from HTTP traffic using Express to isolate TCP socket handling and streamline middleware.

- Repository: [Coder/code-server](https://github.com/coder/code-server)
- Tags: internals
- Published: 2026-03-01

---

**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`](https://github.com/coder/code-server/blob/main/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`](https://github.com/coder/code-server/blob/main/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:

```typescript
// 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`](https://github.com/coder/code-server/blob/main/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:

```typescript
// 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`](https://github.com/coder/code-server/blob/main/src/node/app.ts), the `createApp` function instantiates both routers and binds the upgrade interceptor:

```typescript
// 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`](https://github.com/coder/code-server/blob/main/src/node/routes/index.ts):

```typescript
// 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`](https://github.com/coder/code-server/blob/main/src/node/routes/vscode.ts) demonstrates practical application of this architecture:

```typescript
// 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`](https://github.com/coder/code-server/blob/main/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`](https://github.com/coder/code-server/blob/main/src/node/app.ts) keep WebSocket and HTTP middleware stacks completely isolated.
- **Route mounting** in [`src/node/routes/index.ts`](https://github.com/coder/code-server/blob/main/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.