1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 08:09:08 -05:00

fix(ext/http): resource leak on HttpConn.close() (#11805)

This commit adds tracking of resources that are related
to "HttpConn" so they can be closed automatically
when closing the connection.
This commit is contained in:
Bartek Iwańczuk 2021-08-23 16:15:59 +02:00 committed by GitHub
parent 2c17045aa8
commit 2187c11e5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 50 additions and 2 deletions

View file

@ -815,3 +815,27 @@ unitTest(
} }
}, },
); );
// https://github.com/denoland/deno/issues/11743
unitTest(
{ perms: { net: true } },
async function httpServerDoesntLeakResources() {
const listener = Deno.listen({ port: 4505 });
const [conn, clientConn] = await Promise.all([
listener.accept(),
Deno.connect({ port: 4505 }),
]);
const httpConn = Deno.serveHttp(conn);
await Promise.all([
httpConn.nextRequest(),
clientConn.write(new TextEncoder().encode(
`GET / HTTP/1.1\r\nHost: 127.0.0.1:4505\r\n\r\n`,
)),
]);
httpConn.close();
listener.close();
clientConn.close();
},
);

View file

@ -24,6 +24,10 @@
ArrayPrototypePush, ArrayPrototypePush,
ArrayPrototypeSome, ArrayPrototypeSome,
Promise, Promise,
Set,
SetPrototypeAdd,
SetPrototypeDelete,
SetPrototypeValues,
StringPrototypeIncludes, StringPrototypeIncludes,
StringPrototypeToLowerCase, StringPrototypeToLowerCase,
StringPrototypeSplit, StringPrototypeSplit,
@ -38,6 +42,11 @@
class HttpConn { class HttpConn {
#rid = 0; #rid = 0;
// This set holds resource ids of resources
// that were created during lifecycle of this request.
// When the connection is closed these resources should be closed
// as well.
managedResources = new Set();
constructor(rid) { constructor(rid) {
this.#rid = rid; this.#rid = rid;
@ -85,7 +94,8 @@
/** @type {ReadableStream<Uint8Array> | undefined} */ /** @type {ReadableStream<Uint8Array> | undefined} */
let body = null; let body = null;
if (typeof requestRid === "number") { if (typeof requestRid === "number") {
body = createRequestBodyStream(requestRid); SetPrototypeAdd(this.managedResources, requestRid);
body = createRequestBodyStream(this, requestRid);
} }
const innerRequest = newInnerRequest( const innerRequest = newInnerRequest(
@ -97,6 +107,7 @@
const signal = abortSignal.newSignal(); const signal = abortSignal.newSignal();
const request = fromInnerRequest(innerRequest, signal, "immutable"); const request = fromInnerRequest(innerRequest, signal, "immutable");
SetPrototypeAdd(this.managedResources, responseSenderRid);
const respondWith = createRespondWith( const respondWith = createRespondWith(
this, this,
responseSenderRid, responseSenderRid,
@ -108,6 +119,13 @@
/** @returns {void} */ /** @returns {void} */
close() { close() {
for (const rid of SetPrototypeValues(this.managedResources)) {
try {
core.close(rid);
} catch (_e) {
// pass, might have already been closed
}
}
core.close(this.#rid); core.close(this.#rid);
} }
@ -178,6 +196,7 @@
respBody = new Uint8Array(0); respBody = new Uint8Array(0);
} }
SetPrototypeDelete(httpConn.managedResources, responseSenderRid);
let responseBodyRid; let responseBodyRid;
try { try {
responseBodyRid = await core.opAsync("op_http_response", [ responseBodyRid = await core.opAsync("op_http_response", [
@ -200,6 +219,7 @@
// If `respond` returns a responseBodyRid, we should stream the body // If `respond` returns a responseBodyRid, we should stream the body
// to that resource. // to that resource.
if (responseBodyRid !== null) { if (responseBodyRid !== null) {
SetPrototypeAdd(httpConn.managedResources, responseBodyRid);
try { try {
if (respBody === null || !(respBody instanceof ReadableStream)) { if (respBody === null || !(respBody instanceof ReadableStream)) {
throw new TypeError("Unreachable"); throw new TypeError("Unreachable");
@ -231,6 +251,7 @@
} finally { } finally {
// Once all chunks are sent, and the request body is closed, we can // Once all chunks are sent, and the request body is closed, we can
// close the response body. // close the response body.
SetPrototypeDelete(httpConn.managedResources, responseBodyRid);
try { try {
await core.opAsync("op_http_response_close", responseBodyRid); await core.opAsync("op_http_response_close", responseBodyRid);
} catch { /* pass */ } } catch { /* pass */ }
@ -280,7 +301,7 @@
}; };
} }
function createRequestBodyStream(requestRid) { function createRequestBodyStream(httpConn, requestRid) {
return new ReadableStream({ return new ReadableStream({
type: "bytes", type: "bytes",
async pull(controller) { async pull(controller) {
@ -298,6 +319,7 @@
} else { } else {
// We have reached the end of the body, so we close the stream. // We have reached the end of the body, so we close the stream.
controller.close(); controller.close();
SetPrototypeDelete(httpConn.managedResources, requestRid);
core.close(requestRid); core.close(requestRid);
} }
} catch (err) { } catch (err) {
@ -305,10 +327,12 @@
// error. // error.
controller.error(err); controller.error(err);
controller.close(); controller.close();
SetPrototypeDelete(httpConn.managedResources, requestRid);
core.close(requestRid); core.close(requestRid);
} }
}, },
cancel() { cancel() {
SetPrototypeDelete(httpConn.managedResources, requestRid);
core.close(requestRid); core.close(requestRid);
}, },
}); });