diff --git a/ext/fetch/23_request.js b/ext/fetch/23_request.js
index 70e00a874e..873d05a2b5 100644
--- a/ext/fetch/23_request.js
+++ b/ext/fetch/23_request.js
@@ -9,7 +9,7 @@
///
///
-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,
diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts
index 8978c4f7e0..74628ace18 100644
--- a/tests/unit/serve_test.ts
+++ b/tests/unit/serve_test.ts
@@ -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() {