Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 29 additions & 18 deletions packages/backend/src/server/ServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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<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));
}

/**
Expand Down
Loading