Skip to content

Commit c1b83b3

Browse files
clay-goodclaude
andcommitted
fix(telemetry): dispose the response body so no socket outlives shutdown
undici keeps the connection occupied until the response body is consumed or canceled, and telemetry never reads it — on both the success and non-2xx paths the socket could linger after shutdown() returned. Cancel the body before the tracked promise resolves, with coverage for both paths (bodyUsed asserted after shutdown), and the live endpoint re-verified with disposal in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4a1c6fd commit c1b83b3

2 files changed

Lines changed: 30 additions & 0 deletions

File tree

src/telemetry/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ const pendingEvents = new Set<Promise<void>>();
4141
async function safeTelemetryFetch(url: string, options: RequestInit): Promise<Response> {
4242
try {
4343
const response = await fetch(url, options);
44+
// Telemetry never reads the body, but undici keeps the connection
45+
// occupied until the body is consumed or canceled — dispose of it on
46+
// every path so no socket outlives shutdown().
47+
if (response.body) {
48+
await response.body.cancel();
49+
}
4450
if (response.ok) {
4551
return response;
4652
}

test/telemetry/index.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,30 @@ describe('telemetry/index', () => {
164164
await trackCommand('test', '1.0.0');
165165
await expect(shutdown()).resolves.not.toThrow();
166166
});
167+
168+
it('should dispose the response body of a successful response before the event settles', async () => {
169+
// Undici holds the connection until the body is consumed or canceled;
170+
// an undisposed body would let the socket outlive shutdown().
171+
enableTelemetry();
172+
const response = new Response('{"status": 1}', { status: 200 });
173+
fetchSpy.mockResolvedValueOnce(response);
174+
175+
await trackCommand('test', '1.0.0');
176+
await shutdown();
177+
178+
expect(response.bodyUsed).toBe(true);
179+
});
180+
181+
it('should dispose the response body of a non-2xx response before the event settles', async () => {
182+
enableTelemetry();
183+
const response = new Response('rate limited', { status: 429 });
184+
fetchSpy.mockResolvedValueOnce(response);
185+
186+
await trackCommand('test', '1.0.0');
187+
await shutdown();
188+
189+
expect(response.bodyUsed).toBe(true);
190+
});
167191
});
168192

169193
describe('shutdown', () => {

0 commit comments

Comments
 (0)