Repository: denoland/deno
Pull request: fix(ext/node): client closing streaming request shouldn't terminate http server



Closes https://github.com/denoland/deno/issues/22820

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -1642,7 +1642,9 @@ export class IncomingMessageForServer extends NodeReadable {
         }
       },
       destroy: (err, cb) => {
-        reader?.cancel().finally(() => cb(err));
+        reader?.cancel().catch(() => {
+          // Don't throw error - it's propagated to the user via 'error' event.
+        }).finally(nextTick(onError, this, err, cb));
       },
     });
     // TODO(@bartlomieju): consider more robust path extraction, e.g:

filename: tests/unit_node/http_test.ts
patch: @@ -1308,6 +1308,86 @@ Deno.test("[node/http] client closing a streaming response doesn't terminate ser
   clearInterval(interval!);
 });
 
+Deno.test("[node/http] client closing a streaming request doesn't terminate server", async () => {
+  let interval: number;
+  let uploadedData = "";
+  let requestError: Error | null = null;
+  const server = http.createServer((req, res) => {
+    res.writeHead(200, { "Content-Type": "text/plain" });
+    interval = setInterval(() => {
+      res.write("Hello, world!\n");
+    }, 100);
+    req.on("data", (chunk) => {
+      uploadedData += chunk.toString();
+    });
+    req.on("end", () => {
+      clearInterval(interval);
+    });
+    req.on("error", (err) => {
+      requestError = err;
+      clearInterval(interval);
+      res.end();
+    });
+  });
+
+  const deferred1 = Promise.withResolvers<void>();
+  server.listen(0, () => {
+    // deno-lint-ignore no-explicit-any
+    const port = (server.address() as any).port;
+
+    // Create a client connection to the server
+    const client = net.createConnection({ port }, () => {
+      const headers = [
+        "POST /upload HTTP/1.1",
+        "Host: localhost",
+        "Content-Type: text/plain",
+        "Transfer-Encoding: chunked",
+        "",
+        "",
+      ].join("\r\n");
+
+      client.write(headers);
+
+      const chunk = "A".repeat(100);
+      let sentChunks = 0;
+
+      function writeChunk() {
+        const chunkHeader = `${chunk.length.toString(16)}\r\n`;
+        client.write(chunkHeader);
+        client.write(chunk);
+        client.write("\r\n");
+        sentChunks++;
+
+        if (sentChunks >= 3) {
+          client.destroy();
+          setTimeout(() => {
+            deferred1.resolve();
+          }, 40);
+        } else {
+          setTimeout(writeChunk, 10);
+        }
+      }
+      writeChunk();
+    });
+  });
+
+  await deferred1.promise;
+  assert(requestError !== null, "Server should have received an error");
+  assert(
+    (requestError! as Error)?.name === "Http",
+    `Expected Http error, got ${(requestError! as Error)?.name}`,
+  );
+  assert(
+    (requestError! as Error)?.message.includes(
+      "error reading a body from connection",
+    ),
+  );
+  assertEquals(server.listening, true);
+  server.close();
+  assertEquals(server.listening, false);
+  clearInterval(interval!);
+});
+
 Deno.test("[node/http] http.request() post streaming body works", async () => {
   const server = http.createServer((req, res) => {
     if (req.method === "POST") {