Repository: denoland/deno
Pull request: fix(ext/http): ensure signal is created iff requested

This correctly creates the `AbortSignal` regardless of when we request it. If the signal is requested after the request has completed, the signal is created in the aborted state.

Using GC counts, we can see a reduction in object creation:

This PR: 440
deno 1.42.4: 1650
deno 1.43.0+b02ffec: 874


Changed files:
filename: ext/fetch/23_request.js
patch: @@ -9,7 +9,7 @@
 /// <reference path="./lib.deno_fetch.d.ts" />
 /// <reference lib="esnext" />
 
-import { core, primordials } from "ext:core/mod.js";
+import { core, internals, primordials } from "ext:core/mod.js";
 const {
   ArrayPrototypeMap,
   ArrayPrototypeSlice,
@@ -269,10 +269,20 @@ class Request {
   /** @type {AbortSignal} */
   get [_signal]() {
     const signal = this[_signalCache];
-    if (signal !== undefined) {
+    // This signal not been created yet, and the request is still in progress
+    if (signal === undefined) {
+      const signal = newSignal();
+      this[_signalCache] = signal;
       return signal;
     }
-    return (this[_signalCache] = newSignal());
+    // This signal has not been created yet, but the request has already completed
+    if (signal === false) {
+      const signal = newSignal();
+      this[_signalCache] = signal;
+      signal[signalAbort](signalAbortError);
+      return signal;
+    }
+    return signal;
   }
   get [_mimeType]() {
     const values = getDecodeSplitHeader(
@@ -593,11 +603,20 @@ const signalAbortError = new DOMException(
 ObjectFreeze(signalAbortError);
 
 function abortRequest(request) {
-  if (request[_signal]) {
+  if (request[_signalCache] !== undefined) {
     request[_signal][signalAbort](signalAbortError);
+  } else {
+    request[_signalCache] = false;
   }
 }
 
+function getCachedAbortSignal(request) {
+  return request[_signalCache];
+}
+
+// For testing
+internals.getCachedAbortSignal = getCachedAbortSignal;
+
 export {
   abortRequest,
   fromInnerRequest,

filename: tests/unit/serve_test.ts
patch: @@ -23,6 +23,7 @@ const {
   addTrailers,
   serveHttpOnListener,
   serveHttpOnConnection,
+  getCachedAbortSignal,
   // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
 } = Deno[Deno.internal];
 
@@ -2838,6 +2839,34 @@ for (const delay of ["delay", "nodelay"]) {
   }
 }
 
+// Test for the internal implementation detail of cached request signals. Ensure that the request's
+// signal is aborted if we try to access it after the request has been completed.
+Deno.test(
+  { permissions: { net: true } },
+  async function httpServerSignalCancelled() {
+    let stashedRequest;
+    const { finished, abort } = await makeServer((req) => {
+      // The cache signal is `undefined` because it has not been requested
+      assertEquals(getCachedAbortSignal(req), undefined);
+      stashedRequest = req;
+      return new Response("ok");
+    });
+    await (await fetch(`http://localhost:${servePort}`)).text();
+    abort();
+    await finished;
+
+    // `false` is a semaphore for a signal that should be aborted on creation
+    assertEquals(getCachedAbortSignal(stashedRequest!), false);
+    // Requesting the signal causes it to be materialized
+    assert(stashedRequest!.signal.aborted);
+    // The cached signal is now a full `AbortSignal`
+    assertEquals(
+      getCachedAbortSignal(stashedRequest!).constructor,
+      AbortSignal,
+    );
+  },
+);
+
 Deno.test(
   { permissions: { net: true } },
   async function httpServerCancelFetch() {