Repository: denoland/deno
Pull request: fix(ext/node): don't fire upgrade event for h2c requests

## Summary

HTTP requests to a `node:http` server hang when:
1. The server has an `"upgrade"` listener (e.g., Vite for WebSocket HMR)
2. A browser/client sends an HTTP/2 cleartext upgrade (`Upgrade: h2c`)

The server was calling `upgradeHttpRaw()` and emitting the `"upgrade"` event
for h2c requests. The listener (expecting WebSocket) ignores h2c, leaving the
upgraded connection hanging indefinitely.

Fix: skip the `"upgrade"` event for `Upgrade: h2c` requests, treating them as
regular HTTP/1.1 requests instead. h2c is a protocol-level upgrade not meant
for user-space upgrade handlers.

Closes #32857

## Reproduction

```json
// deno.json
{ "imports": { "vite": "npm:vite@7.3.1" } }
```

```bash
deno run -A npm:vite
curl --http2 http://localhost:5173/  # hangs before fix, 200 after
```

## Test plan
- [x] `curl --http2` to Vite dev server returns 200 (was hanging)
- [x] `curl --http2-prior-knowledge` still works (HTTP/2)
- [x] Regular HTTP/1.1 still works

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -2342,7 +2342,14 @@ export class ServerImpl extends EventEmitter {
         request.headers.get("upgrade");
       req[kRawHeaders] = request.headers;
 
-      if (req.upgrade && this.listenerCount("upgrade") > 0) {
+      // Don't fire the "upgrade" event for h2c (HTTP/2 cleartext) upgrades.
+      // These are protocol-level upgrades that aren't meant for user-space
+      // handlers (like WebSocket). Treating them as regular requests lets
+      // the server respond normally with HTTP/1.1.
+      if (
+        req.upgrade && req.upgrade.toLowerCase() !== "h2c" &&
+        this.listenerCount("upgrade") > 0
+      ) {
         const { conn, response } = upgradeHttpRaw(request);
         const socket = new Socket({
           handle: new TCP(constants.SERVER, conn),

filename: tests/unit_node/http_test.ts
patch: @@ -2430,3 +2430,40 @@ Deno.test("[node/http] upgrade request can be rejected with non-101 status", asy
 
   await promise;
 });
+
+// Regression test for https://github.com/denoland/deno/issues/32857
+// h2c upgrade requests should not fire the "upgrade" event and should
+// be handled as normal HTTP/1.1 requests.
+Deno.test(
+  "[node/http] h2c upgrade does not hang when upgrade listener exists",
+  { permissions: { net: true } },
+  async () => {
+    const { promise, resolve } = Promise.withResolvers<void>();
+    const server = http.createServer((_req, res) => {
+      res.writeHead(200, { "Content-Type": "text/plain" });
+      res.end("ok");
+    });
+    server.on("upgrade", (_req, socket) => {
+      // This listener exists (e.g. for WebSocket) but should NOT be
+      // triggered for h2c upgrades.
+      socket.end();
+    });
+
+    server.listen(0, "127.0.0.1", async () => {
+      const addr = server.address() as { port: number };
+      // Send a request with Upgrade: h2c header, simulating what browsers do
+      const res = await fetch(`http://127.0.0.1:${addr.port}/`, {
+        headers: {
+          "Connection": "Upgrade, HTTP2-Settings",
+          "Upgrade": "h2c",
+        },
+      });
+      assertEquals(res.status, 200);
+      assertEquals(await res.text(), "ok");
+
+      server.close(() => resolve());
+    });
+
+    await promise;
+  },
+);