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. 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..f5244e9e6c 100644 --- a/packages/backend-openapi-utils/src/testUtils.ts +++ b/packages/backend-openapi-utils/src/testUtils.ts @@ -17,7 +17,7 @@ import { Express } from 'express'; import { Server } from 'node:http'; import { Proxy } from './proxy/setup'; -const proxiesToCleanup: Proxy[] = []; +const proxiesToCleanup: Set = new Set(); /** * !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!! @@ -29,7 +29,7 @@ const proxiesToCleanup: Proxy[] = []; */ export async function wrapServer(app: Express): Promise { const proxy = new Proxy(); - proxiesToCleanup.push(proxy); + proxiesToCleanup.add(proxy); await proxy.setup(); const server = app.listen(proxy.forwardTo.port); @@ -48,10 +48,12 @@ function registerHooks() { } registered = true; - afterAll(() => { - for (const proxy of proxiesToCleanup) { - proxy.stop(); - } + afterAll(async () => { + const stopPromises = Array.from(proxiesToCleanup).map(proxy => + proxy.stop(), + ); + await Promise.allSettled(stopPromises); + proxiesToCleanup.clear(); }); }