Repository: denoland/deno
Pull request: fix(ext/node): emit 'close' event on ServerResponse object when client aborted the request

closes #24324
closes #26428 

tasks:
- [x] fix the example in #24324
- [x] do not fail on existing tests

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -138,6 +138,7 @@ class FakeSocket extends EventEmitter {
       remotePort?: number | undefined;
       remoteAddress?: string | undefined;
       reader?: ReadableStreamDefaultReader | undefined;
+      signal?: AbortSignal | undefined;
     } = {},
   ) {
     super();
@@ -147,6 +148,10 @@ class FakeSocket extends EventEmitter {
     this.reader = opts.reader;
     this.writable = true;
     this.readable = true;
+    opts.signal?.addEventListener("abort", () => {
+      this.emit("error", opts.signal?.reason ?? new Error("aborted"));
+      this.emit("close");
+    }, { once: true });
   }
 
   setKeepAlive() {}
@@ -1494,7 +1499,7 @@ export const ServerResponse = function (
   this._readable = readable;
   this._resolve = resolve;
   this.socket = socket;
-
+  this.socket?.on("close", () => this.end());
   this._header = "";
 } as unknown as ServerResponseStatic;
 
@@ -1798,6 +1803,11 @@ export class IncomingMessageForServer extends NodeReadable {
     this.socket = socket;
     this.upgrade = null;
     this.rawHeaders = [];
+    socket?.on("error", (e) => {
+      if (this.listenerCount("error") > 0) {
+        this.emit("error", e);
+      }
+    });
   }
 
   get aborted() {
@@ -1922,6 +1932,7 @@ export class ServerImpl extends EventEmitter {
         remotePort: info.remoteAddr.port,
         encrypted: this._encrypted,
         reader: request.body?.getReader(),
+        signal: request.signal,
       });
 
       const req = new IncomingMessageForServer(socket);

filename: tests/unit_node/http_test.ts
patch: @@ -1960,3 +1960,42 @@ Deno.test("[node/http] 'close' event is emitted when request finished", async ()
   await promise;
   assert(socketCloseEmitted);
 });
+
+Deno.test("[node/http] 'close' event is emitted on ServerResponse object when the client aborted the request in the middle", async () => {
+  let responseCloseEmitted = false;
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const server = http.createServer((req, res) => {
+    res.on("close", () => {
+      responseCloseEmitted = true;
+      res.end();
+    });
+
+    // Streams thre response body
+    res.writeHead(200, { "Content-Type": "text/plain" });
+    const interval = setInterval(() => {
+      res.write("Hello, world!\n");
+    }, 100);
+
+    req.on("error", () => {
+      clearInterval(interval);
+      resolve();
+    });
+  });
+
+  server.listen(0, () => {
+    const { port } = server.address() as { port: number };
+    const client = net.createConnection({ port });
+    client.write("GET / HTTP/1.1\r\n");
+    client.write("Host: localhost\r\n");
+    client.write("Connection: close\r\n");
+    client.write("\r\n");
+    client.on("data", () => {
+      // Client aborts the request in the middle
+      client.end();
+    });
+  });
+
+  await promise;
+  await new Promise((resolve) => server.close(resolve));
+  assert(responseCloseEmitted);
+});