fix(backend): route fastify listen/ready/close errors through logger instead of unhandled rejection (#17401)

* fix(backend): route fastify listen/ready/close errors through logger instead of unhandled rejection

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
Caleb Gates 2026-06-14 03:54:29 -04:00 committed by GitHub
commit b125ce1eb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 30 additions and 18 deletions

View file

@ -28,6 +28,7 @@
- Fix: PerUserDriveChart がシステム所有ファイル (userId が null) の更新で `"group"` の非NULL制約違反によりクラッシュする問題を修正 (#17498)
- Fix: センシティブメディア自動検出周りの依存関係・ファイルの解決に失敗する問題を修正
- Fix: フォロワー限定投稿を指名投稿で引用した際に、引用した投稿の公開範囲が意図せず変更される問題を修正
- 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.4

View file

@ -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) => {
if (this.config.chmodSocket) {
fs.chmodSync(this.config.socket!, this.config.chmodSocket);
try {
if (this.config.socket) {
if (fs.existsSync(this.config.socket)) {
fs.unlinkSync(this.config.socket);
}
});
} else {
fastify.listen({ port: this.config.port, host: '0.0.0.0' });
await fastify.listen({ path: this.config.socket });
if (this.config.chmodSocket) {
fs.chmodSync(this.config.socket, this.config.chmodSocket);
}
} 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<void> {
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<void>(resolve => setTimeout(resolve, 5_000)),
]).catch(err => this.logger.error('fastify.close() failed', err as Error));
}
/**