Skip to content

Commit 2ccab59

Browse files
fix(ClientRequest): remove passthrough socket listeners on close (#755)
Co-authored-by: Artem Zakharchenko <kettanaito@gmail.com>
1 parent 46b084a commit 2ccab59

File tree

3 files changed

+70
-7
lines changed

3 files changed

+70
-7
lines changed

src/interceptors/ClientRequest/MockHttpSocket.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { MockSocket } from '../Socket/MockSocket'
1313
import type { NormalizedSocketWriteArgs } from '../Socket/utils/normalizeSocketWriteArgs'
1414
import { isPropertyAccessible } from '../../utils/isPropertyAccessible'
1515
import { baseUrlFromConnectionOptions } from '../Socket/utils/baseUrlFromConnectionOptions'
16-
import { createServerErrorResponse } from '../../utils/responseUtils'
1716
import { createRequestId } from '../../createRequestId'
1817
import { getRawFetchHeaders } from './utils/recordRawHeaders'
1918
import { FetchResponse } from '../../utils/fetchUtils'
@@ -207,9 +206,18 @@ export class MockHttpSocket extends MockSocket {
207206
})
208207
}
209208

210-
// If the developer destroys the socket, destroy the original connection.
211-
this.once('error', (error) => {
212-
socket.destroy(error)
209+
// The client-facing socket can be destroyed in two ways:
210+
// 1. The developer destroys the socket.
211+
// 2. The passthrough socket "close" is forwarded to the socket.
212+
this.once('close', () => {
213+
socket.removeAllListeners()
214+
215+
// If the closure didn't originate from the passthrough socket, destroy it.
216+
if (!socket.destroyed) {
217+
socket.destroy()
218+
}
219+
220+
this.originalSocket = undefined
213221
})
214222

215223
this.address = socket.address.bind(socket)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// @vitest-environment node
2+
/**
3+
* @see https://github.com/mswjs/msw/issues/2537
4+
* @see https://github.com/mswjs/interceptors/pull/755
5+
*/
6+
import { it, expect, beforeAll, afterAll } from 'vitest'
7+
import http from 'node:http'
8+
import { Socket } from 'node:net'
9+
import { HttpServer } from '@open-draft/test-server/http'
10+
import { DeferredPromise } from '@open-draft/deferred-promise'
11+
import { ClientRequestInterceptor } from '../../../../src/interceptors/ClientRequest'
12+
import { waitForClientRequest } from '../../../helpers'
13+
14+
const httpServer = new HttpServer((app) => {
15+
app.get('/resource', async (req, res) => {
16+
res.send('ok')
17+
})
18+
})
19+
20+
const interceptor = new ClientRequestInterceptor()
21+
22+
beforeAll(async () => {
23+
interceptor.apply()
24+
await httpServer.listen()
25+
})
26+
27+
afterAll(async () => {
28+
interceptor.dispose()
29+
await httpServer.close()
30+
})
31+
32+
it('removes all event listeners from a passthrough socket after closing', async () => {
33+
const request = http.get(httpServer.http.url('/resource'), {
34+
headers: { connection: 'close' },
35+
})
36+
const pendingSocket = new DeferredPromise<Socket>()
37+
38+
request.once('socket', (socket) => {
39+
pendingSocket.resolve(socket)
40+
})
41+
42+
const socket = await pendingSocket
43+
const { res, text } = await waitForClientRequest(request)
44+
45+
expect.soft(res.statusCode).toBe(200)
46+
await expect.soft(text()).resolves.toBe('ok')
47+
48+
const passthroughSocket = Reflect.get(socket, 'originalSocket') as Socket
49+
expect(passthroughSocket).toBeInstanceOf(Socket)
50+
51+
await expect
52+
.poll(
53+
// @ts-expect-error Node.js internals
54+
() => passthroughSocket._events
55+
)
56+
.toEqual({})
57+
})

test/modules/http/compliance/http-timeout.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
/**
2-
* @vitest-environment node
3-
*/
1+
// @vitest-environment node
42
import { vi, it, expect, beforeAll, afterEach, afterAll } from 'vitest'
53
import http from 'node:http'
64
import { HttpServer } from '@open-draft/test-server/http'

0 commit comments

Comments
 (0)