From 1c3b90bbd1464b42888a33e2181041251f9aa78b Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Thu, 26 Feb 2026 10:15:01 -0500 Subject: [PATCH 1/3] fix: remove already closed proxies Signed-off-by: aramissennyeydd --- .../backend-openapi-utils/src/proxy/setup.ts | 34 ++++++++++++++++--- .../backend-openapi-utils/src/testUtils.ts | 15 ++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/backend-openapi-utils/src/proxy/setup.ts b/packages/backend-openapi-utils/src/proxy/setup.ts index ed35a0d3c5..0e47e4a5d6 100644 --- a/packages/backend-openapi-utils/src/proxy/setup.ts +++ b/packages/backend-openapi-utils/src/proxy/setup.ts @@ -58,14 +58,40 @@ export class Proxy { this.express.server = server; } - stop() { + async stop() { if (Object.keys(this.#openRequests).length > 0) { throw new Error('There are still open requests'); } - this.server.stop(); + if (!this.express.server) { + throw new Error( + 'Proxy server was not initialized with an express server', + ); + } + const results = await Promise.allSettled([ + this.server.stop(), - // If this isn't expressly closed, it will cause a jest memory leak warning. - this.express.server?.close(); + // If this isn't expressly closed, it will cause a jest memory leak warning. + new Promise((resolve, reject) => { + this.express.server?.close(err => { + if (err) { + reject(err); + } else { + resolve(undefined); + } + }); + }), + ]); + if (results.some(result => result.status === 'rejected')) { + const errors = results + .filter( + (result): result is PromiseRejectedResult => + result.status === 'rejected', + ) + .map(result => result.reason); + throw new Error( + `Failed to stop proxy server: ${errors.map(e => e.message).join(', ')}`, + ); + } } get url() { diff --git a/packages/backend-openapi-utils/src/testUtils.ts b/packages/backend-openapi-utils/src/testUtils.ts index 88bb3db5bf..afb431c75b 100644 --- a/packages/backend-openapi-utils/src/testUtils.ts +++ b/packages/backend-openapi-utils/src/testUtils.ts @@ -16,8 +16,9 @@ import { Express } from 'express'; import { Server } from 'node:http'; import { Proxy } from './proxy/setup'; +import { randomUUID } from 'node:crypto'; -const proxiesToCleanup: Proxy[] = []; +const proxiesToCleanup: Record = {}; /** * !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!! @@ -29,7 +30,8 @@ const proxiesToCleanup: Proxy[] = []; */ export async function wrapServer(app: Express): Promise { const proxy = new Proxy(); - proxiesToCleanup.push(proxy); + const proxyId = randomUUID(); + proxiesToCleanup[proxyId] = proxy; await proxy.setup(); const server = app.listen(proxy.forwardTo.port); @@ -48,10 +50,11 @@ function registerHooks() { } registered = true; - afterAll(() => { - for (const proxy of proxiesToCleanup) { - proxy.stop(); - } + afterAll(async () => { + const stopPromises = Object.entries(proxiesToCleanup).map(([key, proxy]) => + proxy.stop().finally(() => delete proxiesToCleanup[key]), + ); + await Promise.allSettled(stopPromises); }); } From 59dd9b9aedb81035b642aa055474d53fcb644732 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Thu, 26 Feb 2026 10:15:44 -0500 Subject: [PATCH 2/3] add changeset Signed-off-by: aramissennyeydd --- .changeset/shiny-ghosts-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-ghosts-fold.md diff --git a/.changeset/shiny-ghosts-fold.md b/.changeset/shiny-ghosts-fold.md new file mode 100644 index 0000000000..2425ed0975 --- /dev/null +++ b/.changeset/shiny-ghosts-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-openapi-utils': patch +--- + +Fixes a memory leak during `wrapServer` where stopped servers weren't removed from the clean up list. From 10cb26a7bd90d565f39a899d3230ca75cf5b0049 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 8 Apr 2026 21:06:47 -0400 Subject: [PATCH 3/3] use a set instead and clear the set Signed-off-by: aramissennyeydd --- packages/backend-openapi-utils/src/testUtils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/backend-openapi-utils/src/testUtils.ts b/packages/backend-openapi-utils/src/testUtils.ts index afb431c75b..f5244e9e6c 100644 --- a/packages/backend-openapi-utils/src/testUtils.ts +++ b/packages/backend-openapi-utils/src/testUtils.ts @@ -16,9 +16,8 @@ import { Express } from 'express'; import { Server } from 'node:http'; import { Proxy } from './proxy/setup'; -import { randomUUID } from 'node:crypto'; -const proxiesToCleanup: Record = {}; +const proxiesToCleanup: Set = new Set(); /** * !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!! @@ -30,8 +29,7 @@ const proxiesToCleanup: Record = {}; */ export async function wrapServer(app: Express): Promise { const proxy = new Proxy(); - const proxyId = randomUUID(); - proxiesToCleanup[proxyId] = proxy; + proxiesToCleanup.add(proxy); await proxy.setup(); const server = app.listen(proxy.forwardTo.port); @@ -51,10 +49,11 @@ function registerHooks() { registered = true; afterAll(async () => { - const stopPromises = Object.entries(proxiesToCleanup).map(([key, proxy]) => - proxy.stop().finally(() => delete proxiesToCleanup[key]), + const stopPromises = Array.from(proxiesToCleanup).map(proxy => + proxy.stop(), ); await Promise.allSettled(stopPromises); + proxiesToCleanup.clear(); }); }