From 0945fd66066d64b1ff5d30764b522e86e34ae737 Mon Sep 17 00:00:00 2001 From: Caleb CGates Date: Thu, 14 May 2026 11:23:01 -0400 Subject: [PATCH] fix(backend): route fastify listen/ready/close errors through logger instead of unhandled rejection --- CHANGELOG.md | 1 + packages/backend/src/server/ServerService.ts | 47 ++++++++++++-------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 648613809b6..e4e4e7b0af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### Server - Enhance: RSA 署名処理のオフロード +- Fix: Startup and shutdown failures (port-in-use, socket permission denied, plugin timeouts, leaked WebSocket connections) are now reported through the misskey logger instead of an UnhandledPromiseRejectionWarning stack trace ## 2026.5.1 diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index ef9ac81f953..23ead0febac 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -242,16 +242,16 @@ export class ServerService implements OnApplicationShutdown { this.streamingApiServerService.attach(fastify.server); - fastify.server.on('error', err => { - switch ((err as any).code) { + const handleListenError = (err: unknown): void => { + switch ((err as NodeJS.ErrnoException).code) { case 'EACCES': - this.logger.error(`You do not have permission to listen on port ${this.config.port}.`); + this.logger.error(`You do not have permission to listen on ${this.config.socket ?? `port ${this.config.port}`}.`); break; case 'EADDRINUSE': - this.logger.error(`Port ${this.config.port} is already in use by another process.`); + this.logger.error(`${this.config.socket ?? `Port ${this.config.port}`} is already in use by another process.`); break; default: - this.logger.error(err); + this.logger.error(err as Error); break; } @@ -261,28 +261,39 @@ export class ServerService implements OnApplicationShutdown { // disableClustering process.exit(1); } - }); + }; - if (this.config.socket) { - if (fs.existsSync(this.config.socket)) { - fs.unlinkSync(this.config.socket); - } - fastify.listen({ path: this.config.socket }, (err, address) => { + try { + if (this.config.socket) { + if (fs.existsSync(this.config.socket)) { + fs.unlinkSync(this.config.socket); + } + await fastify.listen({ path: this.config.socket }); if (this.config.chmodSocket) { - fs.chmodSync(this.config.socket!, this.config.chmodSocket); + fs.chmodSync(this.config.socket, this.config.chmodSocket); } - }); - } else { - fastify.listen({ port: this.config.port, host: '0.0.0.0' }); + } else { + await fastify.listen({ port: this.config.port, host: '0.0.0.0' }); + } + await fastify.ready(); + } catch (err) { + handleListenError(err); + return; } - - await fastify.ready(); } @bindThis public async dispose(): Promise { await this.streamingApiServerService.detach(); - await this.#fastify.close(); + // fastify@5 close() waits for upgraded WebSocket connections to drain. + // streamingApiServerService.attach() adds raw ws.Server upgrades that + // fastify does not track in its connection registry, so close() can hang + // forever during OnApplicationShutdown. Cap at 5s so PM2/systemd/k8s + // shutdown timeouts aren't held hostage. + await Promise.race([ + this.#fastify.close(), + new Promise(resolve => setTimeout(resolve, 5_000)), + ]).catch(err => this.logger.error('fastify.close() failed', err as Error)); } /**