Skip to content

Commit 5d9a812

Browse files
committed
chore: abstract http message parser
1 parent cff7820 commit 5d9a812

File tree

1 file changed

+91
-84
lines changed

1 file changed

+91
-84
lines changed

src/interceptors/Socket/SocketInterceptor.ts

+91-84
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ export class SocketInterceptor extends Interceptor<SocketEventMap> {
8585
// Otherwise, listen to the original response
8686
// and forward it to the interceptor.
8787
controller.onResponse = (response, isMockedResponse) => {
88-
console.log('onResponse callback')
89-
9088
self.emitter.emit('response', {
9189
requestId,
9290
request,
@@ -130,9 +128,7 @@ class SocketController {
130128
private shouldSuppressEvents = false
131129
private suppressedEvents: Array<[event: string, ...args: Array<unknown>]> = []
132130
private request: Request
133-
private requestParser: typeof HTTPParser
134131
private requestStream?: Readable
135-
private responseParser: typeof HTTPParser
136132
private responseStream?: Readable
137133

138134
constructor(
@@ -142,58 +138,35 @@ class SocketController {
142138
) {
143139
this.url = parseSocketConnectionUrl(normalizedOptions)
144140

145-
// Create the parser later on because a single
146-
// socket can be *reused* for multiple requests.
147-
// The same way, don't free the parser.
148-
this.requestParser = new HTTPParser()
149-
this.requestParser[HTTPParser.kOnHeadersComplete] = (
150-
verionMajor: number,
151-
versionMinor: number,
152-
headers: Array<string>,
153-
idk: number,
154-
path: string,
155-
idk2: undefined,
156-
idk3: undefined,
157-
idk4: boolean
158-
) => {
159-
this.onRequestStart(path, headers)
160-
}
161-
this.requestParser[HTTPParser.kOnBody] = (chunk: Buffer) => {
162-
this.onRequestData(chunk)
163-
}
164-
this.requestParser[HTTPParser.kOnMessageComplete] = () => {
165-
this.onRequestEnd()
166-
}
167-
this.requestParser.initialize(HTTPParser.REQUEST, {})
168-
169-
this.responseParser = new HTTPParser()
170-
this.responseParser[HTTPParser.kOnHeadersComplete] = (
171-
verionMajor: number,
172-
versionMinor: number,
173-
headers: Array<string>,
174-
method: string | undefined,
175-
url: string | undefined,
176-
status: number,
177-
statusText: string,
178-
upgrade: boolean,
179-
shouldKeepAlive: boolean
180-
) => {
181-
this.onResponseStart(status, statusText, headers)
182-
}
183-
this.responseParser[HTTPParser.kOnBody] = (chunk: Buffer) => {
184-
this.onResponseData(chunk)
185-
}
186-
this.responseParser[HTTPParser.kOnMessageComplete] = () => {
187-
this.onResponseEnd()
188-
}
189-
this.responseParser.initialize(
190-
HTTPParser.RESPONSE,
191-
// Don't create any async resources here.
192-
// This has to be "HTTPINCOMINGMESSAGE" in practice.
193-
// @see https://github.com/nodejs/llhttp/issues/44#issuecomment-582499320
194-
// new HTTPServerAsyncResource('INTERCEPTORINCOMINGMESSAGE', socket)
195-
{}
196-
)
141+
const requestParser = new HttpMessageParser('request', {
142+
onHeadersComplete: (major, minor, headers, _, path) => {
143+
this.onRequestStart(path, headers)
144+
},
145+
onBody: (chunk) => {
146+
this.onRequestData(chunk)
147+
},
148+
onMessageComplete: this.onRequestEnd.bind(this),
149+
})
150+
151+
const responseParser = new HttpMessageParser('response', {
152+
onHeadersComplete: (
153+
versionMajor,
154+
versionMinor,
155+
headers,
156+
method,
157+
url,
158+
status,
159+
statusText,
160+
upgrade,
161+
keepalive
162+
) => {
163+
this.onResponseStart(status, statusText, headers)
164+
},
165+
onBody: (chunk) => {
166+
this.onResponseData(chunk)
167+
},
168+
onMessageComplete: this.onResponseEnd.bind(this),
169+
})
197170

198171
socket.emit = new Proxy(socket.emit, {
199172
apply: (target, thisArg, args) => {
@@ -209,13 +182,13 @@ class SocketController {
209182
if (this.shouldSuppressEvents) {
210183
if (args[0] === 'error') {
211184
Reflect.set(this.socket, '_hadError', false)
212-
this.suppressedEvents.push(['error', args.slice(1)])
185+
this.suppressedEvents.push(['error', ...args.slice(1)])
213186
return true
214187
}
215188

216189
// Suppress close events for errored mocked connections.
217190
if (args[0] === 'close') {
218-
this.suppressedEvents.push(['close', args.slice(1)])
191+
this.suppressedEvents.push(['close', ...args.slice(1)])
219192
return true
220193
}
221194
}
@@ -224,7 +197,7 @@ class SocketController {
224197
},
225198
})
226199

227-
socket.once('ready', () => {
200+
socket.once('connect', () => {
228201
// Notify the interceptor once the socket is ready.
229202
// The HTTP parser triggers BEFORE that.
230203
this.onRequest(this.request)
@@ -234,7 +207,7 @@ class SocketController {
234207
socket.write = new Proxy(socket.write, {
235208
apply: (target, thisArg, args) => {
236209
if (args[0] !== null) {
237-
this.requestParser.execute(
210+
requestParser.push(
238211
Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0])
239212
)
240213
}
@@ -246,7 +219,7 @@ class SocketController {
246219
socket.push = new Proxy(socket.push, {
247220
apply: (target, thisArg, args) => {
248221
if (args[0] !== null) {
249-
this.responseParser.execute(
222+
responseParser.push(
250223
Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0])
251224
)
252225
}
@@ -304,11 +277,15 @@ class SocketController {
304277
}
305278

306279
private replayErrors() {
280+
console.log('replay errors...', this.suppressedEvents)
281+
307282
if (this.suppressedEvents.length === 0) {
308283
return
309284
}
310285

311286
for (const [event, ...args] of this.suppressedEvents) {
287+
console.log('replaying event', event, ...args)
288+
312289
if (event === 'error') {
313290
Reflect.set(this.socket, '_hadError', true)
314291
}
@@ -342,6 +319,7 @@ class SocketController {
342319
method,
343320
headers,
344321
body: methodWithBody ? Readable.toWeb(this.requestStream) : null,
322+
// @ts-expect-error Not documented fetch property.
345323
duplex: methodWithBody ? 'half' : undefined,
346324
credentials: 'same-origin',
347325
})
@@ -356,8 +334,6 @@ class SocketController {
356334
}
357335

358336
private onRequestEnd() {
359-
this.requestParser.free()
360-
361337
invariant(
362338
this.requestStream,
363339
'Failed to handle the request end: request stream is missing'
@@ -376,7 +352,7 @@ class SocketController {
376352
statusText,
377353
headers: parseRawHeaders(rawHeaders),
378354
})
379-
this.onResponse(response)
355+
this.onResponse(response, false)
380356
}
381357

382358
private onResponseData(chunk: Buffer) {
@@ -388,8 +364,6 @@ class SocketController {
388364
}
389365

390366
private onResponseEnd() {
391-
this.responseParser.free()
392-
393367
invariant(
394368
this.responseStream,
395369
'Failed to handle the response end: response stream is missing'
@@ -398,6 +372,57 @@ class SocketController {
398372
}
399373
}
400374

375+
type HttpMessageParserMessageType = 'request' | 'response'
376+
interface HttpMessageParserCallbacks<T extends HttpMessageParserMessageType> {
377+
onHeadersComplete?: T extends 'request'
378+
? (
379+
versionMajor: number,
380+
versionMinor: number,
381+
headers: Array<string>,
382+
idk: number,
383+
path: string
384+
) => void
385+
: (
386+
versionMajor: number,
387+
versionMinor: number,
388+
headers: Array<string>,
389+
method: string | undefined,
390+
url: string | undefined,
391+
status: number,
392+
statusText: string,
393+
upgrade: boolean,
394+
shouldKeepAlive: boolean
395+
) => void
396+
onBody?: (chunk: Buffer) => void
397+
onMessageComplete?: () => void
398+
}
399+
400+
class HttpMessageParser<T extends HttpMessageParserMessageType> {
401+
private parser: HTTPParser
402+
403+
constructor(messageType: T, callbacks: HttpMessageParserCallbacks<T>) {
404+
this.parser = new HTTPParser()
405+
this.parser.initialize(
406+
messageType === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
407+
// Don't create any async resources here.
408+
// This has to be "HTTPINCOMINGMESSAGE" in practice.
409+
// @see https://github.com/nodejs/llhttp/issues/44#issuecomment-582499320
410+
// new HTTPServerAsyncResource('INTERCEPTORINCOMINGMESSAGE', socket)
411+
{}
412+
)
413+
this.parser[HTTPParser.kOnHeadersComplete] = callbacks.onHeadersComplete
414+
this.parser[HTTPParser.kOnMessageComplete] = callbacks.onMessageComplete
415+
}
416+
417+
public push(chunk: Buffer): void {
418+
this.parser.execute(chunk)
419+
}
420+
421+
public destroy(): void {
422+
this.parser.free()
423+
}
424+
}
425+
401426
function parseSocketConnectionUrl(
402427
options: NormalizedSocketConnectOptions
403428
): URL {
@@ -429,21 +454,3 @@ function parseRawHeaders(rawHeaders: Array<string>): Headers {
429454
}
430455
return headers
431456
}
432-
433-
// MOCKED REQUEST:
434-
// 1. lookup // mock that's OK
435-
// 2. connect
436-
// 3. ready
437-
// HAS MOCK?
438-
// -> Y: data -> close
439-
// -> N (no response, non-existing host):
440-
// -> replayErrors()
441-
// -> lookup (error), error, close
442-
443-
// BYPASSED REQUEST TO EXISTING HOST:
444-
// 1. lookup (no errors)
445-
// 2. (skip mockConnect), forward all socket events.
446-
// 3. emit "request" on the interceptor.
447-
// 4. HAS MOCK?
448-
// -> Y: respondWith: data -> close
449-
// -> N: do nothing

0 commit comments

Comments
 (0)