fix(rest): prevent AbortSignal event listener leak#11461
fix(rest): prevent AbortSignal event listener leak#11461Sim-hu wants to merge 1 commit intodiscordjs:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~3 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can get early access to new features in CodeRabbit.Enable the |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/rest/src/lib/handlers/Shared.ts (1)
74-80:⚠️ Potential issue | 🟠 Major
{ once: true }does not prevent listener retention on non-aborted requestsLine 79 only auto-removes the listener if
abortis dispatched. For requests that complete successfully or with errors where the user signal never aborts, the listener remains attached on a reused signal and retains the per-request controller closure. Retries reuse the samerequestDataobject (including its signal) inSequentialHandler.tsandBurstHandler.ts, accumulating listeners on long-lived signals. Explicitly unregister the listener in thefinallyblock.Proposed fix
export async function makeNetworkRequest( manager: REST, routeId: RouteData, url: string, options: RequestInit, requestData: HandlerRequestData, retries: number, ) { const controller = new AbortController(); + const userSignal = requestData.signal; + let onAbort: (() => void) | null = null; const timeout = setTimeout( () => controller.abort(), normalizeTimeout(manager.options.timeout, routeId.bucketRoute, requestData.body), ); - if (requestData.signal) { + if (userSignal) { // If the user signal was aborted, abort the controller, else abort the local signal. // The reason why we don't re-use the user's signal, is because users may use the same signal for multiple // requests, and we do not want to cause unexpected side-effects. - if (requestData.signal.aborted) controller.abort(); - else requestData.signal.addEventListener('abort', () => controller.abort(), { once: true }); + if (userSignal.aborted) controller.abort(); + else { + onAbort = () => controller.abort(); + userSignal.addEventListener('abort', onAbort, { once: true }); + } } @@ } finally { clearTimeout(timeout); + if (userSignal && onAbort) { + userSignal.removeEventListener('abort', onAbort); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/rest/src/lib/handlers/Shared.ts` around lines 74 - 80, The abort listener added to requestData.signal via requestData.signal.addEventListener('abort', () => controller.abort(), { once: true }) only removes itself when the signal is aborted, leaking the per-request controller closure for requests that complete normally or error without abort; modify the code path that creates the listener (in Shared.ts where controller is created and requestData.signal is referenced) to capture the listener function reference, add it with addEventListener, and always remove it in the request cleanup/finally block (call requestData.signal.removeEventListener with that listener) so retries that reuse requestData (e.g., SequentialHandler.ts and BurstHandler.ts) do not accumulate listeners or retain closures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@packages/rest/src/lib/handlers/Shared.ts`:
- Around line 74-80: The abort listener added to requestData.signal via
requestData.signal.addEventListener('abort', () => controller.abort(), { once:
true }) only removes itself when the signal is aborted, leaking the per-request
controller closure for requests that complete normally or error without abort;
modify the code path that creates the listener (in Shared.ts where controller is
created and requestData.signal is referenced) to capture the listener function
reference, add it with addEventListener, and always remove it in the request
cleanup/finally block (call requestData.signal.removeEventListener with that
listener) so retries that reuse requestData (e.g., SequentialHandler.ts and
BurstHandler.ts) do not accumulate listeners or retain closures.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 72931abe-1507-46b3-ac5e-32d37407171b
📒 Files selected for processing (1)
packages/rest/src/lib/handlers/Shared.ts
Summary
{ once: true }to theabortevent listener on the user-providedAbortSignalinmakeNetworkRequest(packages/rest/src/lib/handlers/Shared.ts)AbortSignalacross multiple requests, listeners accumulate, each capturing a reference to the localAbortControllerand preventing it from being garbage collectedonceoption ensures the listener is automatically removed after firing and allows the capturedcontrollerto be garbage collected when it is no longer neededTest plan
@discordjs/restpackage compiles without type errors (tsc --noEmitpasses)getEventListenerscount in Node.js or by observing memory profiles under repeated requests with a shared signal)