Skip to content

Commit 32fcd0f

Browse files
committed
fix: patch node:net early; add http tests
1 parent 2bc1c9f commit 32fcd0f

2 files changed

Lines changed: 166 additions & 26 deletions

File tree

src/interceptors/net/index.ts

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,46 @@ export interface SocketConnectionEventMap {
1616
]
1717
}
1818

19+
const kImplementation = Symbol('kImplementation')
20+
const kRestoreValue = Symbol('kRestoreValue')
21+
22+
/**
23+
* Apply a transparent proxy to the "node:net" module on the module's scope.
24+
* This way, this interceptor can function if it gets imported before the surface
25+
* that relies on "node:net", like "node:http" or "undici".
26+
*
27+
* @note You MUST import the interceptor BEFORE the surface relying on "node:net".
28+
*/
29+
const { createConnection } = net
30+
Object.defineProperties(net.createConnection, {
31+
[kRestoreValue]: {
32+
value: createConnection.bind(createConnection),
33+
enumerable: true,
34+
},
35+
[kImplementation]: {
36+
value() {
37+
return Reflect.get(net.createConnection, kRestoreValue)
38+
},
39+
enumerable: true,
40+
writable: true,
41+
},
42+
})
43+
44+
function createSwitchableProxy(target: any) {
45+
return new Proxy(target, {
46+
apply(target, thisArg, argArray) {
47+
return Reflect.apply(
48+
Reflect.get(target, kImplementation),
49+
thisArg,
50+
argArray
51+
)
52+
},
53+
})
54+
}
55+
56+
net.connect = createSwitchableProxy(net.connect)
57+
net.createConnection = createSwitchableProxy(net.createConnection)
58+
1959
export class SocketInterceptor extends Interceptor<SocketConnectionEventMap> {
2060
static symbol = Symbol('SocketInterceptor')
2161

@@ -24,20 +64,21 @@ export class SocketInterceptor extends Interceptor<SocketConnectionEventMap> {
2464
}
2565

2666
protected setup(): void {
27-
const {
28-
connect: originalConnect,
29-
createConnection: originalCreateConnection,
30-
} = net
67+
const originalConnect = Reflect.get(net.connect, kRestoreValue)
68+
const originalCreateConnection = Reflect.get(
69+
net.createConnection,
70+
kRestoreValue
71+
)
3172

32-
net.connect = (...args: Array<unknown>) => {
73+
Reflect.set(net.connect, kImplementation, (...args: Array<unknown>) => {
3374
const [options, connectionListener] = normalizeNetConnectArgs(
3475
args as NetConnectArgs
3576
)
3677
const socket = new MockSocket({
3778
...args,
3879
onConnect: connectionListener,
3980
createConnection() {
40-
return originalConnect.apply(originalCreateConnection, args as any)
81+
return originalConnect.apply(originalConnect, args as any)
4182
},
4283
})
4384

@@ -47,31 +88,39 @@ export class SocketInterceptor extends Interceptor<SocketConnectionEventMap> {
4788
})
4889

4990
return socket
50-
}
91+
})
5192

52-
net.createConnection = (...args: Array<unknown>) => {
53-
const [options] = normalizeNetConnectArgs(args as NetConnectArgs)
54-
const socket = new MockSocket({
55-
...args,
56-
createConnection() {
57-
return originalCreateConnection.apply(
58-
originalCreateConnection,
59-
args as any
60-
)
61-
},
62-
})
93+
Reflect.set(
94+
net.createConnection,
95+
kImplementation,
96+
(...args: Array<unknown>) => {
97+
const [options] = normalizeNetConnectArgs(args as NetConnectArgs)
98+
const socket = new MockSocket({
99+
...args,
100+
createConnection() {
101+
return originalCreateConnection.apply(
102+
originalCreateConnection,
103+
args as any
104+
)
105+
},
106+
})
63107

64-
this.emitter.emit('connection', {
65-
options,
66-
socket,
67-
})
108+
this.emitter.emit('connection', {
109+
options,
110+
socket,
111+
})
68112

69-
return socket
70-
}
113+
return socket
114+
}
115+
)
71116

72117
this.subscriptions.push(() => {
73-
net.connect = originalConnect
74-
net.createConnection = originalCreateConnection
118+
Reflect.set(net.connect, kImplementation, originalConnect)
119+
Reflect.set(
120+
net.createConnection,
121+
kImplementation,
122+
originalCreateConnection
123+
)
75124
})
76125
}
77126
}

test/modules/net/net.http.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// @vitest-environment node
2+
import { SocketInterceptor } from '../../../src/interceptors/net'
3+
import http from 'node:http'
4+
import { beforeAll, afterEach, afterAll, vi, it, expect } from 'vitest'
5+
import { DeferredPromise } from '@open-draft/deferred-promise'
6+
import { waitForClientRequest } from '../../helpers'
7+
8+
export const interceptor = new SocketInterceptor()
9+
10+
beforeAll(() => {
11+
interceptor.apply()
12+
})
13+
14+
afterEach(() => {
15+
interceptor.removeAllListeners()
16+
})
17+
18+
afterAll(() => {
19+
interceptor.dispose()
20+
})
21+
22+
it('intercepts an http request without any body', async () => {
23+
const requestHeader = new DeferredPromise<string>()
24+
const errorListener = vi.fn()
25+
26+
interceptor.on('connection', ({ socket }) => {
27+
socket.on('error', errorListener)
28+
socket.once('write', (chunk) => requestHeader.resolve(chunk))
29+
socket.push('HTTP/1.1 200 OK\r\n\r\n')
30+
socket.push('hello world')
31+
socket.push(null)
32+
})
33+
34+
const request = http.get('http://localhost/resource')
35+
const { res, text } = await waitForClientRequest(request)
36+
37+
await expect.soft(requestHeader).resolves.toBe(`GET /resource HTTP/1.1\r
38+
Host: localhost\r
39+
Connection: close\r
40+
\r
41+
`)
42+
43+
expect.soft(res.statusCode).toBe(200)
44+
await expect.soft(text()).resolves.toBe('hello world')
45+
expect.soft(errorListener).not.toHaveBeenCalled()
46+
})
47+
48+
it('intercepts an http request with chunked request body', async () => {
49+
const requestChunks: Array<string> = []
50+
const errorListener = vi.fn()
51+
52+
interceptor.on('connection', ({ socket }) => {
53+
socket.on('error', errorListener)
54+
socket.on('write', (chunk) => requestChunks.push(chunk.toString()))
55+
socket.push('HTTP/1.1 200 OK\r\n\r\n')
56+
socket.push('hello world')
57+
socket.push(null)
58+
})
59+
60+
const request = http.request('http://localhost/resource', { method: 'POST' })
61+
request.write('request')
62+
request.end('body')
63+
const { res, text } = await waitForClientRequest(request)
64+
65+
expect.soft(requestChunks.join('\r\n')).toBe(
66+
`POST /resource HTTP/1.1\r
67+
Host: localhost\r
68+
Connection: close\r
69+
Transfer-Encoding: chunked\r
70+
\r
71+
7\r
72+
\r
73+
\r
74+
request\r
75+
\r
76+
\r
77+
4\r
78+
\r
79+
\r
80+
body\r
81+
\r
82+
\r
83+
0\r
84+
\r
85+
`
86+
)
87+
88+
expect.soft(res.statusCode).toBe(200)
89+
await expect.soft(text()).resolves.toBe('hello world')
90+
expect.soft(errorListener).not.toHaveBeenCalled()
91+
})

0 commit comments

Comments
 (0)