From 465e568d6e681723e410aa9646bcd3139c4da56f Mon Sep 17 00:00:00 2001 From: rowkav09 Date: Sat, 10 Oct 2026 03:54:48 +0100 Subject: [PATCH] fix: destroy the discord client when login fails A client whose login failed was never adopted, so close() could not release it. It is now destroyed on the failure path; cleanup errors are swallowed so they cannot delay or replace the original login error. --- src/discord-rpc.js | 8 +++++++- test/discord-rpc-login-cleanup.test.js | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test/discord-rpc-login-cleanup.test.js diff --git a/src/discord-rpc.js b/src/discord-rpc.js index c1731f1c..1835bc58 100644 --- a/src/discord-rpc.js +++ b/src/discord-rpc.js @@ -25,7 +25,13 @@ export function createDiscordRpcTransport({ clientId, createClient } = {}) { if (!next || typeof next.login !== "function" || typeof next.setActivity !== "function" || typeof next.clearActivity !== "function") { throw new TypeError("RPC client: expected login, setActivity and clearActivity functions"); } - await next.login({ clientId }); + try { await next.login({ clientId }); } + catch (error) { + // A failed client was never adopted, so close() cannot release it. + // Cleanup must not delay or replace the original login failure. + try { Promise.resolve(next.destroy?.()).catch(() => {}); } catch {} + throw error; + } // A close during createClient/login must not be undone by this late // arrival: destroy the new client instead of adopting it. if (epoch !== generation) { diff --git a/test/discord-rpc-login-cleanup.test.js b/test/discord-rpc-login-cleanup.test.js new file mode 100644 index 00000000..42a9e632 --- /dev/null +++ b/test/discord-rpc-login-cleanup.test.js @@ -0,0 +1,22 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {createDiscordRpcTransport} from '../src/discord-rpc.js'; +for(const mode of ['sync','async','throwing','rejecting','pending']){ + test(`failed RPC login destroys discarded client (${mode}) and preserves error`,async()=>{ + let destroyed=0,created=0,timer; + const error=new Error('fixture login failed'); + const factory=()=>({login:async()=>{throw error;},setActivity:async()=>{},clearActivity:async()=>{},destroy:()=>{destroyed++;if(mode==='async')return Promise.resolve();if(mode==='throwing')throw new Error('cleanup');if(mode==='rejecting')return Promise.reject(new Error('cleanup'));if(mode==='pending')return new Promise(()=>{});}}); + const transport=createDiscordRpcTransport({clientId:'123456789012345678',createClient:async()=>{created++;return factory();}}); + try{ + for(let i=1;i<=2;i++){ + await assert.rejects(Promise.race([transport.connect(),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('cleanup blocked')),100);})]),e=>e===error); + clearTimeout(timer);assert.equal(destroyed,i);assert.equal(created,i);assert.equal(transport.connected,false); + } + await transport.close();assert.equal(destroyed,2); + }finally{clearTimeout(timer);} + }); +} +test('failed RPC login without destroy preserves error',async()=>{ + const error=new Error('fixture');const transport=createDiscordRpcTransport({clientId:'123456789012345678',createClient:async()=>({login:async()=>{throw error;},setActivity:async()=>{},clearActivity:async()=>{}})}); + await assert.rejects(transport.connect(),e=>e===error); +});