Skip to content

Commit 29ec8f4

Browse files
committed
fix(wip): proxy events between interceptors
1 parent d5c838f commit 29ec8f4

File tree

11 files changed

+132
-156
lines changed

11 files changed

+132
-156
lines changed

pnpm-lock.yaml

Lines changed: 1 addition & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/interceptors/ClientRequest/index.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
11
import http from 'node:http'
22
import https from 'node:https'
3-
import { HttpRequestEventMap } from '#/src/events/http'
4-
import { Interceptor } from '#/src/Interceptor'
53
import { runInRequestContext } from '#/src/request-context'
64
import { applyPatch } from '#/src/utils/apply-patch'
75
import { HttpRequestInterceptor } from '#/src/interceptors/http'
8-
import { propagateHttpEvents } from '#/src/utils/interceptor-utils'
6+
import { Interceptor } from '#/src/Interceptor'
7+
import { HttpRequestEventMap } from '#/src/events/http'
8+
import { proxyEventListeners } from '#/src/utils/interceptor-utils'
99

1010
export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
11-
static symbol = Symbol('client-request-interceptor')
11+
static symbol = Symbol.for('client-request-interceptor')
12+
13+
#httpInterceptor: HttpRequestInterceptor
1214

1315
constructor() {
1416
super(ClientRequestInterceptor.symbol)
17+
18+
this.#httpInterceptor = new HttpRequestInterceptor()
19+
this.subscriptions.push(
20+
proxyEventListeners({
21+
from: this.emitter,
22+
to: this.#httpInterceptor['emitter'],
23+
filter: (event) => {
24+
return event.initiator instanceof http.ClientRequest
25+
},
26+
})
27+
)
1528
}
1629

1730
protected setup(): void {
18-
const httpInterceptor = new HttpRequestInterceptor()
19-
20-
httpInterceptor.apply()
21-
this.subscriptions.push(() => httpInterceptor.dispose())
22-
23-
const { controller } = propagateHttpEvents(
24-
httpInterceptor['emitter'],
25-
this.emitter,
26-
(event) => {
27-
return event.initiator instanceof http.ClientRequest
28-
}
29-
)
30-
this.subscriptions.push(() => controller.abort())
31+
this.#httpInterceptor.apply()
32+
this.subscriptions.push(() => this.#httpInterceptor.dispose())
3133

3234
this.subscriptions.push(
3335
applyPatch(http, 'ClientRequest', (ClientRequest) => {

src/interceptors/XMLHttpRequest/node.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,48 @@
1+
import { Emitter } from 'rettime'
12
import { requestContext } from '#/src/request-context'
23
import { hasConfigurableGlobal } from '#/src/utils/hasConfigurableGlobal'
3-
import { applyPatch } from '#/src/utils/apply-patch'
44
import { Interceptor } from '#/src/Interceptor'
5-
import { HttpRequestEventMap } from '../../events/http'
65
import { HttpRequestInterceptor } from '#/src/interceptors/http'
6+
import { applyPatch } from '#/src/utils/apply-patch'
77
import { FetchRequest } from '#/src/utils/fetchUtils'
8-
import { propagateHttpEvents } from '#/src/utils/interceptor-utils'
8+
import { HttpRequestEventMap } from '#/src/events/http'
9+
import { proxyEventListeners } from '#/src/utils/interceptor-utils'
910

1011
export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
1112
static symbol = Symbol.for('xhr-interceptor')
1213

14+
#httpInterceptor: HttpRequestInterceptor
15+
1316
constructor() {
1417
super(XMLHttpRequestInterceptor.symbol)
18+
19+
this.#httpInterceptor = new HttpRequestInterceptor()
20+
this.subscriptions.push(
21+
proxyEventListeners({
22+
from: this.emitter,
23+
to: this.#httpInterceptor['emitter'],
24+
filter: (event) => {
25+
if (event.initiator instanceof XMLHttpRequest) {
26+
event.request = this.#transformRequest(
27+
event.request,
28+
event.initiator
29+
)
30+
return true
31+
}
32+
33+
return false
34+
},
35+
})
36+
)
1537
}
1638

1739
protected checkEnvironment() {
1840
return hasConfigurableGlobal('XMLHttpRequest')
1941
}
2042

2143
protected setup(): void {
22-
const httpInterceptor = new HttpRequestInterceptor()
23-
24-
httpInterceptor.apply()
25-
this.subscriptions.push(() => httpInterceptor.dispose())
26-
27-
this.emitter.hooks.on('beforeEmit', (event) => {
28-
event.modify = true
29-
})
30-
31-
const { controller } = propagateHttpEvents(
32-
httpInterceptor['emitter'],
33-
this.emitter,
34-
(event) => {
35-
if (event.initiator instanceof XMLHttpRequest) {
36-
event.request = this.#transformRequest(event.request, event.initiator)
37-
return true
38-
}
39-
40-
return false
41-
}
42-
)
43-
this.subscriptions.push(() => controller.abort())
44+
this.#httpInterceptor.apply()
45+
this.subscriptions.push(() => this.#httpInterceptor.dispose())
4446

4547
this.logger.info('patching global "XMLHttpRequest"...')
4648

@@ -70,11 +72,19 @@ export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap>
7072
}
7173

7274
#transformRequest(request: Request, initiator: XMLHttpRequest): Request {
75+
const expectedCredentials = initiator.withCredentials
76+
? 'include'
77+
: 'same-origin'
78+
79+
if (request.credentials === expectedCredentials) {
80+
return request
81+
}
82+
7383
return new FetchRequest(request.url, {
7484
...request,
7585
method: request.method,
7686
headers: request.headers,
77-
credentials: initiator.withCredentials ? 'include' : 'same-origin',
87+
credentials: expectedCredentials,
7888
body: request.body,
7989
})
8090
}

src/interceptors/fetch/node.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
1-
import { Interceptor } from '#/src/Interceptor'
2-
import type { HttpRequestEventMap } from '#/src/events/http'
31
import { hasConfigurableGlobal } from '#/src/utils/hasConfigurableGlobal'
42
import { canParseUrl } from '#/src/utils/canParseUrl'
53
import { requestContext } from '#/src/request-context'
64
import { applyPatch } from '#/src/utils/apply-patch'
75
import { HttpRequestInterceptor } from '#/src/interceptors/http'
8-
import { propagateHttpEvents } from '#/src/utils/interceptor-utils'
6+
import { Interceptor } from '#/src/Interceptor'
7+
import { HttpRequestEventMap } from '#/src/events/http'
8+
import { proxyEventListeners } from '#/src/utils/interceptor-utils'
99

1010
export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
1111
static symbol = Symbol.for('fetch-interceptor')
1212

13+
#httpInterceptor: HttpRequestInterceptor
14+
1315
constructor() {
1416
super(FetchInterceptor.symbol)
17+
18+
this.#httpInterceptor = new HttpRequestInterceptor()
19+
this.subscriptions.push(
20+
proxyEventListeners({
21+
from: this.emitter,
22+
to: this.#httpInterceptor['emitter'],
23+
filter: (event) => {
24+
return event.initiator instanceof XMLHttpRequest
25+
},
26+
})
27+
)
1528
}
1629

1730
protected checkEnvironment() {
1831
return hasConfigurableGlobal('fetch')
1932
}
2033

2134
protected setup(): void {
22-
const httpInterceptor = new HttpRequestInterceptor()
23-
24-
httpInterceptor.apply()
25-
this.subscriptions.push(() => httpInterceptor.dispose())
26-
27-
const { controller } = propagateHttpEvents(
28-
httpInterceptor['emitter'],
29-
this.emitter,
30-
(event) => {
31-
return event.initiator instanceof Request
32-
}
33-
)
34-
this.subscriptions.push(() => controller.abort())
35+
this.#httpInterceptor.apply()
36+
this.subscriptions.push(() => this.#httpInterceptor.dispose())
3537

3638
this.subscriptions.push(
3739
applyPatch(globalThis, 'fetch', (realFetch) => {

src/interceptors/http/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,10 @@ export class HttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
296296
callback?.()
297297
}
298298

299-
responseSocket._destroy = () => {
299+
responseSocket._destroy = (
300+
_error: Error | null,
301+
callback: (error: Error | null) => void
302+
) => {
300303
/**
301304
* Destroy the socket if the response stream errored.
302305
* @see https://github.com/mswjs/interceptors/issues/738
@@ -306,6 +309,7 @@ export class HttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
306309
* @see https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/_http_client.js#L586
307310
*/
308311
socket.destroy()
312+
callback(null)
309313
}
310314

311315
serverResponse.assignSocket(responseSocket)

src/utils/handleRequest.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,7 @@ export async function handleRequest(
114114
controller: options.controller,
115115
}
116116
const requestEvent = new HttpRequestEvent(requestEventData)
117-
const requestListenersPromise = options.emitter.emitAsPromise(
118-
requestEvent
119-
)
117+
const requestListenersPromise = options.emitter.emitAsPromise(requestEvent)
120118

121119
await Promise.race([
122120
// Short-circuit the request handling promise if the request gets aborted.
@@ -152,7 +150,6 @@ export async function handleRequest(
152150
// If the developer has added "unhandledException" listeners,
153151
// allow them to handle the error. They can translate it to a
154152
// mocked response, network error, or forward it as-is.
155-
console.log('[DEBUG handleRequest] unhandledException listenerCount:', options.emitter.listenerCount('unhandledException'))
156153
if (options.emitter.listenerCount('unhandledException') > 0) {
157154
// Create a new request controller just for the unhandled exception case.
158155
// This is needed because the original controller might have been already

src/utils/interceptor-utils.ts

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,45 @@
1-
import { DefaultEventMap, Emitter, EventMap } from 'rettime'
1+
import { Emitter } from 'rettime'
22

3-
export function propagateHttpEvents<SourceEventMap extends DefaultEventMap>(
4-
source: Emitter<SourceEventMap>,
5-
destination: Emitter<any>,
6-
predicate: (event: EventMap.Events<SourceEventMap>) => boolean
7-
) {
3+
export function proxyEventListeners<T extends Emitter<any>>(options: {
4+
from: T
5+
to: T
6+
filter: (event: Emitter.Events<T>) => boolean
7+
}) {
88
const controller = new AbortController()
99

10-
const propagateEvent = async (event: EventMap.Events<SourceEventMap>) => {
11-
if (predicate(event)) {
12-
await destination.emitAsPromise(event)
10+
const propagateEvent = async (event: Emitter.Events<T>) => {
11+
if (options.filter(event)) {
12+
await options.from.emitAsPromise(event)
1313
}
1414
}
1515

16-
source.on(
17-
'*',
18-
async (event) => {
19-
if (event.type !== 'response') {
20-
await propagateEvent(event)
21-
}
22-
},
23-
{ signal: controller.signal }
24-
)
25-
26-
/**
27-
* @note Lazily add a "response" listener to the HTTP interceptor if this
28-
* interceptor receives a response listener. HTTP interceptor creates a
29-
* response parser only if a "response" listener is present.
30-
*
31-
* Cannot use hooks for this because `removeAllListeners()` in rettime
32-
* also removes hooks listeners, breaking lazy registration across tests.
33-
*/
34-
destination.hooks.on(
16+
options.from.hooks.on(
3517
'newListener',
3618
(type) => {
37-
if (
38-
type === 'response' &&
39-
!source.listeners('response').includes(propagateEvent)
40-
) {
41-
source.on('response', propagateEvent, { signal: controller.signal })
19+
if (!options.to.listeners(type).includes(propagateEvent)) {
20+
options.to.on(type, propagateEvent, { signal: controller.signal })
4221
}
4322
},
44-
{ signal: controller.signal }
23+
{
24+
persist: true,
25+
signal: controller.signal,
26+
}
4527
)
4628

47-
destination.hooks.on(
29+
options.from.hooks.on(
4830
'removeListener',
4931
(type) => {
50-
if (
51-
type === 'response' &&
52-
source.listeners('response').includes(propagateEvent)
53-
) {
54-
source.removeListener('response', propagateEvent)
32+
if (options.from.listenerCount(type) === 1) {
33+
options.to.removeListener(type, propagateEvent)
5534
}
5635
},
5736
{
37+
persist: true,
5838
signal: controller.signal,
5939
}
6040
)
6141

62-
return {
63-
controller,
42+
return () => {
43+
controller.abort()
6444
}
6545
}

0 commit comments

Comments
 (0)