Skip to content

Commit

Permalink
fix(ext/http): ensure signal is created iff requested (#23601)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mmastrac committed Apr 29, 2024
1 parent da52058 commit 56fec53
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 4 deletions.
27 changes: 23 additions & 4 deletions ext/fetch/23_request.js
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/serve_test.ts
Expand Up @@ -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];

Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 56fec53

Please sign in to comment.