fix(ssh): contain relay notification handler failures (#2463)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-05-20 17:49:08 -07:00
committed by GitHub
co-authored by Orca
parent 3dcc9e69da
commit 75f7510b1f
4 changed files with 155 additions and 5 deletions
@@ -229,6 +229,50 @@ describe('SshChannelMultiplexer', () => {
expect(a).not.toHaveBeenCalled()
expect(b).toHaveBeenCalledWith({ streamId: 7 })
})
it('contains generic notification handler failures', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const badHandler = vi.fn(() => {
throw new Error('subscriber exploded')
})
const goodHandler = vi.fn()
mux.onNotification(badHandler)
mux.onNotification(goodHandler)
expect(() =>
transport.dataCallbacks[0](makeNotificationFrame('pty.data', { id: 'pty-1' }, 1))
).not.toThrow()
expect(badHandler).toHaveBeenCalled()
expect(goodHandler).toHaveBeenCalledWith('pty.data', { id: 'pty-1' })
expect(mux.isDisposed()).toBe(false)
expect(warnSpy).toHaveBeenCalledWith(
'[ssh-mux] Notification handler failed for pty.data: subscriber exploded'
)
})
it('contains method notification handler failures', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const badHandler = vi.fn(() => {
throw new Error('stream consumer exploded')
})
const goodHandler = vi.fn()
mux.onNotificationByMethod('fs.streamChunk', badHandler)
mux.onNotificationByMethod('fs.streamChunk', goodHandler)
expect(() =>
transport.dataCallbacks[0](
makeNotificationFrame('fs.streamChunk', { streamId: 1, seq: 0, data: 'aGk=' }, 1)
)
).not.toThrow()
expect(badHandler).toHaveBeenCalled()
expect(goodHandler).toHaveBeenCalledWith({ streamId: 1, seq: 0, data: 'aGk=' })
expect(mux.isDisposed()).toBe(false)
expect(warnSpy).toHaveBeenCalledWith(
'[ssh-mux] Method notification handler failed for fs.streamChunk: stream consumer exploded'
)
})
})
describe('keepalive', () => {
+22 -2
View File
@@ -361,13 +361,33 @@ export class SshChannelMultiplexer {
// collection and skips the next handler. Iterating a snapshot prevents that.
const snapshot = Array.from(this.notificationHandlers)
for (const handler of snapshot) {
handler(msg.method, params)
try {
handler(msg.method, params)
} catch (err) {
// Why: relay notifications arrive on the SSH stream callback; one
// bad subscriber must not escape as a main-process uncaught exception.
console.warn(
`[ssh-mux] Notification handler failed for ${msg.method}: ${
err instanceof Error ? err.message : String(err)
}`
)
}
}
const methodHandlers = this.methodNotificationHandlers.get(msg.method)
if (methodHandlers && methodHandlers.size > 0) {
const methodSnapshot = Array.from(methodHandlers)
for (const handler of methodSnapshot) {
handler(params)
try {
handler(params)
} catch (err) {
// Why: file-stream and PTY listeners are per-method subscribers; keep
// the mux alive even if one consumer rejects a malformed notification.
console.warn(
`[ssh-mux] Method notification handler failed for ${msg.method}: ${
err instanceof Error ? err.message : String(err)
}`
)
}
}
}
}