From 320b68a14c17d4079585f6ed7bd675c0408ac28d Mon Sep 17 00:00:00 2001 From: Zachary Belford Date: Sat, 30 Aug 2025 22:03:17 -0700 Subject: [PATCH] Handle transport start/stop failures --- src/server.test.ts | 31 +++++++++++++++++++++++++++++++ src/server.ts | 28 +++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/server.test.ts b/src/server.test.ts index 868763e..bf7e0f5 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -186,4 +186,35 @@ describe('Server', () => { expect(transport.stop).toHaveBeenCalled(); }); + + it('rolls back started transports if a later transport fails to start', async () => { + const server = new Server({ openrpcDocument: {} as any }); + + const first = createTestTransport(); + const second = createTestTransport(); + const error = new Error('boom'); + + first.start.mockResolvedValue(undefined); + second.start.mockRejectedValue(error); + + (server as any).transports = [first, second]; + + await expect(server.start()).rejects.toThrow('boom'); + expect(first.stop).toHaveBeenCalled(); + expect(second.stop).not.toHaveBeenCalled(); + }); + + it('continues stopping transports when one fails', async () => { + const server = new Server({ openrpcDocument: {} as any }); + + const first = createTestTransport(); + const second = createTestTransport(); + first.stop.mockRejectedValue(new Error('fail')); + second.stop.mockResolvedValue(undefined); + + (server as any).transports = [first, second]; + + await expect(server.stop()).rejects.toThrow('fail'); + expect(second.stop).toHaveBeenCalled(); + }); }); diff --git a/src/server.ts b/src/server.ts index 0a6cb4e..78e2f69 100644 --- a/src/server.ts +++ b/src/server.ts @@ -72,14 +72,36 @@ export default class Server { } public async start() { - for (const transport of this.transports) { - await transport.start(); + const started: typeof this.transports = []; + try { + for (const transport of this.transports) { + await transport.start(); + started.push(transport); + } + } catch (e) { + for (const transport of started.reverse()) { + try { + await transport.stop(); + } catch (_) { + // ignore rollback errors + } + } + throw e; } } public async stop() { + const errors: Error[] = []; for (const transport of this.transports) { - await transport.stop(); + try { + await transport.stop(); + } catch (err) { + errors.push(err as Error); + } + } + if (errors.length > 0) { + const message = errors.map((err) => err.message).join('; '); + throw new Error(`Failed to stop transports: ${message}`); } }