fix: close remote runtime subscriptions after protocol errors (#4257)

This commit is contained in:
Neil 2026-05-31 09:49:45 -07:00 committed by GitHub
parent 2be4430d57
commit 9d730453ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 1 deletions

View File

@ -90,6 +90,42 @@ describe('subscribeRemoteRuntimeRequest', () => {
offSpy.mockRestore()
}
})
it('closes established subscription sockets after terminal protocol errors', async () => {
const offSpy = vi.spyOn(WebSocketClient.prototype, 'off')
try {
const server = await createSubscriptionServer({ sendMismatchedResponseAfterSubscribe: true })
const onResponse = vi.fn()
const onError = vi.fn()
const onClose = vi.fn()
const subscription = await subscribeRemoteRuntimeRequest(
server.pairing,
'terminal.subscribe',
{ terminal: 't1' },
1000,
{
onResponse,
onError,
onClose
}
)
await vi.waitFor(() => expect(onResponse).toHaveBeenCalled())
await vi.waitFor(() =>
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ code: 'invalid_runtime_response' })
)
)
expect(onClose).toHaveBeenCalledOnce()
const removedEvents = offSpy.mock.calls.map(([event]) => event)
expect(removedEvents).toEqual(expect.arrayContaining(['open', 'error', 'close', 'message']))
expect(subscription.sendBinary(new Uint8Array([9]))).toBe(false)
} finally {
offSpy.mockRestore()
}
})
})
describe('sendRemoteRuntimeRequest', () => {
@ -129,7 +165,11 @@ describe('sendRemoteRuntimeRequest', () => {
})
})
async function createSubscriptionServer(): Promise<{
async function createSubscriptionServer(
options: {
sendMismatchedResponseAfterSubscribe?: boolean
} = {}
): Promise<{
pairing: PairingOffer
nextBinary: Promise<Uint8Array>
}> {
@ -186,6 +226,15 @@ async function createSubscriptionServer(): Promise<{
result: { type: 'subscribed' },
_meta: { runtimeId: 'runtime-test' }
})
if (options.sendMismatchedResponseAfterSubscribe) {
sendEncrypted(ws, sharedKey, {
id: `${request.id}-mismatch`,
ok: true,
streaming: true,
result: { type: 'subscribed' },
_meta: { runtimeId: 'runtime-test' }
})
}
})
})

View File

@ -411,6 +411,11 @@ export async function subscribeRemoteRuntimeRequest<TResult>(
return
}
callbacks.onError(error)
// Why: after a subscription is established, protocol failures are
// terminal for this socket. Closing here releases the WebSocket listeners
// and lets the IPC subscription registry drop its retained callbacks.
closeSocketAfterCleanup()
callbacks.onClose?.()
}
try {