Completed requests leak listeners on caller-provided AbortSignals
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 74/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- typescript
Research direction
Start in src/core.ts at fetchWithTimeout(), especially lines 549-550, and trace how requests settle across success, error, timeout, and retry paths. Add coverage for listener cleanup on each path and verify that reusing one caller-provided AbortSignal leaves no listeners after completed requests.
Written by the indexing model from the issue text.
Description
Description
Every request made with a caller-provided AbortSignal permanently adds an abort listener to that signal.
At src/core.ts:549-550, fetchWithTimeout() calls:
if (signal) signal.addEventListener('abort', () => controller.abort());
The listener is never removed after the request settles. Reusing one signal across a batch therefore retains one internal AbortController closure per completed request.
Reproduction
import { getEventListeners } from 'node:events';
import Browserbase from '@browserbasehq/sdk';
const controller = new AbortController();
const client = new Browserbase({
apiKey: 'test',
maxRetries: 0,
fetch: async () =>
new Response('{}', {
status: 200,
headers: { 'content-type': 'application/json' },
}),
});
for (let i = 0; i < 12; i++) {
await client.get('/ok', { signal: controller.signal });
}
console.log(getEventListeners(controller.signal, 'abort').length); // 12
Expected behavior
A completed request removes its abort forwarding listener, leaving zero listeners after the loop.
Actual behavior
All 12 listeners remain attached. Retries add additional listeners because each attempt creates another controller.
Why this matters
Long-lived applications commonly share a signal across a batch of requests. Listener accumulation retains completed request state and can produce unbounded memory growth for large batches. The forwarding callback should be registered once/removed in cleanup (or use an equivalent combined-signal mechanism), with coverage for success, error, timeout, and retry paths.
Tested against @browserbasehq/sdk 2.18.0 / current main (b781bd7).
- Dominant language
- TypeScript
- Stars
- 64
- Forks
- 17
- Avg merge
- 13m
- Merged PRs (30d)
- 4
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from browserbase/sdk-node
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
browserbase/sdk-node#202 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
browserbase/sdk-node#197 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
browserbase/sdk-node#193 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
browserbase/sdk-node#180 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 66/100
browserbase/sdk-node#218 · 2 comments ·
All issues in browserbase/sdk-node
Similar issues
-
VerificationGate: ATTRIBUTION quote guard never matches a normal quotation (\b around the quote) Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
danielmiessler/LifeOS#2234 ·
-
T: Bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
Mend: dependency security vulnerability untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 70/100