test-proxy-recorder 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/proxy.js CHANGED
@@ -23,7 +23,7 @@ function parseCliArgs() {
23
23
  "Port number for the proxy server",
24
24
  String(DEFAULT_PORT)
25
25
  ).option(
26
- "-r, --recordings-dir <path>",
26
+ "-d, --dir <path>",
27
27
  "Directory to store recordings (relative to CWD)",
28
28
  DEFAULT_RECORDINGS_DIR
29
29
  ).action(() => {
@@ -39,7 +39,7 @@ function parseCliArgs() {
39
39
  if (targets2.length === 0) {
40
40
  program.help();
41
41
  }
42
- const recordingsDir2 = path.resolve(process.cwd(), options.recordingsDir);
42
+ const recordingsDir2 = path.resolve(process.cwd(), options.dir);
43
43
  return { targets: targets2, port: port2, recordingsDir: recordingsDir2 };
44
44
  }
45
45
 
@@ -84,13 +84,23 @@ async function loadRecordingSession(filePath) {
84
84
  return JSON.parse(fileContent);
85
85
  }
86
86
  function processRecordings(recordings) {
87
- const keySequenceMap = /* @__PURE__ */ new Map();
88
- return recordings.map((recording) => {
87
+ const recordingsByKey = /* @__PURE__ */ new Map();
88
+ for (const recording of recordings) {
89
89
  const key = recording.key;
90
- const currentSeq = keySequenceMap.get(key) || 0;
91
- keySequenceMap.set(key, currentSeq + 1);
92
- return { ...recording, sequence: currentSeq };
93
- });
90
+ if (!recordingsByKey.has(key)) {
91
+ recordingsByKey.set(key, []);
92
+ }
93
+ recordingsByKey.get(key).push(recording);
94
+ }
95
+ const processedRecordings = [];
96
+ for (const [_key, keyRecordings] of recordingsByKey) {
97
+ keyRecordings.sort((a, b) => a.recordingId - b.recordingId);
98
+ keyRecordings.forEach((recording, index) => {
99
+ processedRecordings.push({ ...recording, sequence: index });
100
+ });
101
+ }
102
+ processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
103
+ return processedRecordings;
94
104
  }
95
105
  async function saveRecordingSession(recordingsDir2, session) {
96
106
  const filePath = getRecordingPath(recordingsDir2, session.id);
@@ -108,16 +118,19 @@ async function saveRecordingSession(recordingsDir2, session) {
108
118
  `Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
109
119
  );
110
120
  }
111
- function getReqID(req) {
112
- const urlParts = req.url.split("?");
113
- const pathname = urlParts[0];
114
- const query = urlParts[1] || "";
121
+ function generateRecordingKey(pathname, query, method) {
115
122
  const pathPart = pathname === "/" ? "root" : pathname.slice(1);
116
123
  const normalizedPath = filenamify2(pathPart, { replacement: "_" });
117
124
  const queryHash = generateQueryHash(query);
118
- const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
125
+ const filename = `${method}_${normalizedPath}${queryHash}.json`;
119
126
  return filenamify2(filename, { replacement: "_" });
120
127
  }
128
+ function getReqID(req) {
129
+ const urlParts = req.url.split("?");
130
+ const pathname = urlParts[0];
131
+ const query = urlParts[1] || "";
132
+ return generateRecordingKey(pathname, query, req.method);
133
+ }
121
134
  function generateQueryHash(query) {
122
135
  if (!query) {
123
136
  return "";
@@ -153,19 +166,25 @@ var ProxyServer = class {
153
166
  recordingsDir;
154
167
  recordingIdCounter;
155
168
  // Unique ID for each recording entry
169
+ sequenceCounterByKey;
170
+ // Sequence counter per key (endpoint)
156
171
  replaySessions;
157
172
  // Track multiple concurrent replay sessions by recording ID
173
+ recordingPromises;
174
+ // Stack of promises that resolve to completed recordings
158
175
  constructor(targets2, recordingsDir2) {
159
176
  this.targets = targets2;
160
177
  this.currentTargetIndex = 0;
161
178
  this.mode = Modes.transparent;
162
179
  this.recordingId = null;
163
180
  this.recordingIdCounter = 0;
181
+ this.sequenceCounterByKey = /* @__PURE__ */ new Map();
164
182
  this.replayId = null;
165
183
  this.modeTimeout = null;
166
184
  this.currentSession = null;
167
185
  this.recordingsDir = recordingsDir2;
168
186
  this.replaySessions = /* @__PURE__ */ new Map();
187
+ this.recordingPromises = [];
169
188
  this.proxy = httpProxy.createProxyServer({
170
189
  secure: false,
171
190
  changeOrigin: true,
@@ -191,7 +210,7 @@ var ProxyServer = class {
191
210
  }
192
211
  setupProxyEventHandlers() {
193
212
  this.proxy.on("error", this.handleProxyError.bind(this));
194
- this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
213
+ this.proxy.on("proxyRes", this.addCorsHeaders.bind(this));
195
214
  }
196
215
  handleProxyError(err, req, res) {
197
216
  console.error("Proxy error:", err);
@@ -207,12 +226,6 @@ var ProxyServer = class {
207
226
  }
208
227
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
209
228
  }
210
- handleProxyResponse(proxyRes, req) {
211
- this.addCorsHeaders(proxyRes, req);
212
- if (this.mode === Modes.record && this.recordingId) {
213
- this.recordResponse(req, proxyRes);
214
- }
215
- }
216
229
  /**
217
230
  * Get CORS headers for a given request
218
231
  * @param req The incoming HTTP request
@@ -270,7 +283,15 @@ var ProxyServer = class {
270
283
  * @returns The recording ID, or null if not found
271
284
  */
272
285
  getRecordingIdFromRequest(req) {
273
- return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
286
+ const fromHeader = this.getRecordingIdFromHeader(req);
287
+ const fromCookie = this.getRecordingIdFromCookie(req);
288
+ if (fromHeader) {
289
+ return fromHeader;
290
+ }
291
+ if (fromCookie) {
292
+ return fromCookie;
293
+ }
294
+ return null;
274
295
  }
275
296
  /**
276
297
  * Get or create a replay session state for a given recording ID
@@ -295,6 +316,27 @@ var ProxyServer = class {
295
316
  }
296
317
  return session;
297
318
  }
319
+ /**
320
+ * Clean up a session - removes it from memory and resets counters
321
+ * @param sessionId The session ID to clean up
322
+ */
323
+ async cleanupSession(sessionId) {
324
+ if (this.replaySessions.has(sessionId)) {
325
+ console.log(`[CLEANUP] Removing replay session: ${sessionId}`);
326
+ this.replaySessions.delete(sessionId);
327
+ }
328
+ if (this.recordingId === sessionId) {
329
+ console.log(`[CLEANUP] Saving and clearing active recording session: ${sessionId}`);
330
+ await this.saveCurrentSession();
331
+ this.currentSession = null;
332
+ this.recordingId = null;
333
+ }
334
+ if (this.replayId === sessionId) {
335
+ console.log(`[CLEANUP] Clearing active replay session: ${sessionId}`);
336
+ this.replayId = null;
337
+ }
338
+ console.log(`[CLEANUP] Session ${sessionId} cleaned up successfully`);
339
+ }
298
340
  parseGetParams(req) {
299
341
  const url = new URL(req.url || "", `http://${req.headers.host}`);
300
342
  const mode = url.searchParams.get("mode");
@@ -306,19 +348,41 @@ var ProxyServer = class {
306
348
  }
307
349
  return { mode, id, timeout };
308
350
  }
351
+ async parseControlRequest(req) {
352
+ if (req.method === "GET") {
353
+ return this.parseGetParams(req);
354
+ }
355
+ if (req.method === "POST") {
356
+ const body = await readRequestBody(req);
357
+ console.log(`MODE CHANGE (${req.method})`, body);
358
+ return JSON.parse(body);
359
+ }
360
+ throw new Error("Unsupported control method");
361
+ }
309
362
  async handleControlRequest(req, res) {
363
+ if (req.method === "GET") {
364
+ sendJsonResponse(res, HTTP_STATUS_OK, {
365
+ recordingsDir: this.recordingsDir,
366
+ mode: this.mode,
367
+ id: this.recordingId || this.replayId
368
+ });
369
+ return;
370
+ }
310
371
  try {
311
- let data;
312
- if (req.method === "GET") {
313
- data = this.parseGetParams(req);
314
- } else if (req.method === "POST") {
315
- const body = await readRequestBody(req);
316
- console.log(`MODE CHANGE (${req.method})`, body);
317
- data = JSON.parse(body);
318
- } else {
372
+ const data = await this.parseControlRequest(req);
373
+ const { mode, id, timeout: requestTimeout, cleanup } = data;
374
+ if (cleanup && id) {
375
+ await this.cleanupSession(id);
376
+ sendJsonResponse(res, HTTP_STATUS_OK, {
377
+ success: true,
378
+ message: `Session ${id} cleaned up`,
379
+ mode: this.mode
380
+ });
319
381
  return;
320
382
  }
321
- const { mode, id, timeout: requestTimeout } = data;
383
+ if (!mode) {
384
+ throw new Error("Mode parameter is required when cleanup is not specified");
385
+ }
322
386
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
323
387
  this.clearModeTimeout();
324
388
  await this.switchMode(mode, id);
@@ -334,7 +398,8 @@ var ProxyServer = class {
334
398
  success: true,
335
399
  mode: this.mode,
336
400
  id: this.recordingId || this.replayId,
337
- timeout
401
+ timeout,
402
+ recordingsDir: this.recordingsDir
338
403
  });
339
404
  } catch (error) {
340
405
  console.error("Control request error:", error);
@@ -350,7 +415,7 @@ var ProxyServer = class {
350
415
  async switchMode(mode, id) {
351
416
  console.log(`Switching to ${mode.toUpperCase()} mode`);
352
417
  if (this.currentSession && this.mode === Modes.record) {
353
- await this.saveCurrentSession(true);
418
+ await this.saveCurrentSession();
354
419
  console.log("Session saved, continuing with mode switch");
355
420
  }
356
421
  switch (mode) {
@@ -390,6 +455,8 @@ var ProxyServer = class {
390
455
  this.recordingId = id;
391
456
  this.replayId = null;
392
457
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
458
+ this.recordingIdCounter = 0;
459
+ this.sequenceCounterByKey.clear();
393
460
  console.log(`Switched to record mode with ID: ${id}`);
394
461
  }
395
462
  async switchToReplayMode(id) {
@@ -407,171 +474,119 @@ var ProxyServer = class {
407
474
  console.log(`Switched to replay mode with ID: ${id}`);
408
475
  }
409
476
  setupModeTimeout(timeout) {
477
+ clearTimeout(this.modeTimeout || 0);
410
478
  this.modeTimeout = setTimeout(async () => {
411
479
  console.log("Timeout reached, switching back to transparent mode");
412
- await this.saveCurrentSession(true);
480
+ await this.saveCurrentSession();
413
481
  this.switchToTransparentMode();
414
482
  this.modeTimeout = null;
415
483
  }, timeout);
416
484
  }
417
- async saveCurrentSession(filterIncomplete = false) {
418
- if (!this.currentSession) {
485
+ async flushPendingRecordings() {
486
+ if (this.recordingPromises.length === 0) {
419
487
  return;
420
488
  }
421
- if (filterIncomplete) {
422
- const incompleteCount = this.currentSession.recordings.filter(
423
- (r) => !r.response
424
- ).length;
425
- if (incompleteCount > 0) {
426
- this.currentSession.recordings = this.currentSession.recordings.filter(
427
- (r) => r.response
428
- );
489
+ const results = await Promise.allSettled(this.recordingPromises);
490
+ if (this.currentSession) {
491
+ for (const result of results) {
492
+ if (result.status === "fulfilled" && result.value) {
493
+ this.currentSession.recordings.push(result.value);
494
+ }
429
495
  }
496
+ console.log(
497
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
498
+ );
430
499
  }
431
- console.log(
432
- `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
433
- );
434
- await saveRecordingSession(this.recordingsDir, this.currentSession);
500
+ this.recordingPromises = [];
435
501
  }
436
- saveRequestRecordSync(req, body) {
502
+ async saveCurrentSession() {
437
503
  if (!this.currentSession) {
438
504
  return;
439
505
  }
440
- const key = getReqID(req);
441
- const recordingId = this.recordingIdCounter++;
442
- req.__recordingId = recordingId;
443
- const record = {
444
- request: {
445
- method: req.method,
446
- url: req.url,
447
- headers: req.headers,
448
- body: body || null
449
- },
450
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
451
- key,
452
- recordingId
453
- };
454
- this.currentSession.recordings.push(record);
506
+ await this.flushPendingRecordings();
455
507
  console.log(
456
- // eslint-disable-next-line sonarjs/no-nested-template-literals
457
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
508
+ `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
458
509
  );
510
+ await saveRecordingSession(this.recordingsDir, this.currentSession);
459
511
  }
460
- updateRequestBodySync(req, body) {
461
- if (!this.currentSession) {
462
- return;
512
+ getRecordingIdOrError(req, res) {
513
+ const recordingIdFromRequest = this.getRecordingIdFromRequest(req);
514
+ if (recordingIdFromRequest) {
515
+ return recordingIdFromRequest;
463
516
  }
464
- const recordingId = req.__recordingId;
465
- if (recordingId === void 0) {
466
- console.error(
467
- `updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
517
+ if (this.replaySessions.size > 1) {
518
+ console.warn(
519
+ `[CONCURRENT REPLAY WARNING] Request to ${req.method} ${req.url} is missing ${RECORDING_ID_HEADER} header/cookie. Active sessions: ${[...this.replaySessions.keys()].join(", ")}. this.replayId fallback would be: ${this.replayId} (NOT USING - could be wrong session)`
468
520
  );
469
- return;
470
- }
471
- const record = this.currentSession.recordings.find(
472
- (r) => r.recordingId === recordingId
473
- );
474
- if (!record) {
475
- console.error(
476
- `updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
521
+ const corsHeaders = this.getCorsHeaders(req);
522
+ res.writeHead(HTTP_STATUS_BAD_REQUEST, {
523
+ "Content-Type": "application/json",
524
+ ...corsHeaders
525
+ });
526
+ res.end(
527
+ JSON.stringify({
528
+ error: "Missing recording ID in concurrent replay mode. Ensure x-test-rcrd-id header is set.",
529
+ activeSessions: [...this.replaySessions.keys()],
530
+ hint: "This usually means page.setExtraHTTPHeaders() did not apply to this request type"
531
+ })
477
532
  );
478
- return;
533
+ return null;
534
+ }
535
+ const recordingId = this.replayId;
536
+ if (!recordingId) {
537
+ const corsHeaders = this.getCorsHeaders(req);
538
+ res.writeHead(HTTP_STATUS_BAD_REQUEST, {
539
+ "Content-Type": "application/json",
540
+ ...corsHeaders
541
+ });
542
+ res.end(JSON.stringify({ error: "No replay session active" }));
543
+ return null;
479
544
  }
480
- record.request.body = body || null;
481
545
  console.log(
482
- `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
546
+ `[FALLBACK] Using replayId fallback for ${req.method} ${req.url} -> session: ${recordingId} (single session mode)`
483
547
  );
548
+ return recordingId;
484
549
  }
485
- async recordResponse(req, proxyRes) {
486
- if (!this.currentSession) {
487
- return;
488
- }
489
- const recordingId = req.__recordingId;
490
- if (recordingId === void 0) {
491
- console.error(
492
- `recordResponse: No recording ID found on request ${req.method} ${req.url}`
493
- );
494
- return;
550
+ async ensureSessionLoaded(recordingId, filePath) {
551
+ const sessionState = this.getOrCreateReplaySession(recordingId);
552
+ if (!sessionState.loadedSession) {
553
+ sessionState.loadedSession = await loadRecordingSession(filePath);
554
+ console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
495
555
  }
496
- const record = this.currentSession.recordings.find(
497
- (r) => r.recordingId === recordingId
498
- );
499
- if (!record) {
500
- console.error(
501
- `recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
502
- );
503
- return;
504
- }
505
- const chunks = [];
506
- proxyRes.on("data", (chunk) => {
507
- chunks.push(chunk);
508
- });
509
- proxyRes.on("end", async () => {
510
- const body = Buffer.concat(chunks).toString("utf8");
511
- record.response = {
512
- statusCode: proxyRes.statusCode,
513
- headers: proxyRes.headers,
514
- body: body || null
515
- };
516
- console.log(
517
- `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
518
- );
519
- });
556
+ return sessionState;
520
557
  }
521
- async recordResponseData(req, proxyRes, body) {
522
- if (!this.currentSession) {
523
- return false;
558
+ getServedTracker(sessionState, key) {
559
+ if (!sessionState.servedRecordingIdsByKey.has(key)) {
560
+ sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
524
561
  }
525
- const recordingId = req.__recordingId;
526
- if (recordingId === void 0) {
527
- console.error(
528
- `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
529
- );
530
- return false;
562
+ return sessionState.servedRecordingIdsByKey.get(key);
563
+ }
564
+ selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
565
+ for (const rec of recordsWithKey) {
566
+ if (!servedForThisKey.has(rec.recordingId)) {
567
+ return rec;
568
+ }
531
569
  }
532
- const record = this.currentSession.recordings.find(
533
- (r) => r.recordingId === recordingId
534
- );
535
- if (!record) {
536
- console.error(
537
- `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
570
+ if (recordsWithKey.length > 0) {
571
+ console.log(
572
+ `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
538
573
  );
539
- return false;
574
+ return recordsWithKey[recordsWithKey.length - 1];
540
575
  }
541
- record.response = {
542
- statusCode: proxyRes.statusCode,
543
- headers: proxyRes.headers,
544
- body: body || null
545
- };
546
- console.log(
547
- `recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
548
- );
549
- return true;
576
+ return null;
550
577
  }
551
578
  async handleReplayRequest(req, res) {
552
- const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
553
- if (!recordingId) {
554
- const corsHeaders = this.getCorsHeaders(req);
555
- res.writeHead(HTTP_STATUS_BAD_REQUEST, {
556
- "Content-Type": "application/json",
557
- ...corsHeaders
558
- });
559
- res.end(JSON.stringify({ error: "No replay session active" }));
560
- return;
561
- }
579
+ const recordingId = this.getRecordingIdOrError(req, res);
580
+ if (!recordingId) return;
562
581
  const key = getReqID(req);
563
582
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
564
583
  try {
565
- const sessionState = this.getOrCreateReplaySession(recordingId);
566
- if (!sessionState.loadedSession) {
567
- sessionState.loadedSession = await loadRecordingSession(filePath);
568
- console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
569
- }
584
+ const sessionState = await this.ensureSessionLoaded(
585
+ recordingId,
586
+ filePath
587
+ );
570
588
  const session = sessionState.loadedSession;
571
- if (!sessionState.servedRecordingIdsByKey.has(key)) {
572
- sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
573
- }
574
- const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
589
+ const servedForThisKey = this.getServedTracker(sessionState, key);
575
590
  const host = req.headers.host || "unknown";
576
591
  const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
577
592
  const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
@@ -600,30 +615,23 @@ var ProxyServer = class {
600
615
  }
601
616
  const requestCount = servedForThisKey.size + 1;
602
617
  console.log(
603
- `[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
618
+ `[replay request #${requestCount}] ${req.method} ${req.url} (key: ${key}, session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
604
619
  );
605
- let record;
606
- for (const rec of recordsWithKey) {
607
- if (!servedForThisKey.has(rec.recordingId)) {
608
- record = rec;
609
- break;
610
- }
611
- }
612
- if (!record) {
613
- console.log(
614
- `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
620
+ const record = this.selectReplayRecord(
621
+ recordsWithKey,
622
+ servedForThisKey,
623
+ key,
624
+ recordingId
625
+ );
626
+ if (!record || !record.response) {
627
+ throw new Error(
628
+ `No response recorded for this request: ${req.method} ${host}${req.url}`
615
629
  );
616
- record = recordsWithKey[recordsWithKey.length - 1];
617
630
  }
618
631
  servedForThisKey.add(record.recordingId);
619
632
  console.log(
620
633
  `[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
621
634
  );
622
- if (!record.response) {
623
- throw new Error(
624
- `No response recorded for this request: ${req.method} ${host}${req.url}`
625
- );
626
- }
627
635
  const { statusCode, headers, body } = record.response;
628
636
  const responseHeaders = {
629
637
  ...headers,
@@ -678,82 +686,124 @@ var ProxyServer = class {
678
686
  const target = this.getTarget();
679
687
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
680
688
  if (this.mode === Modes.record) {
681
- this.saveRequestRecordSync(req, null);
682
- await this.bufferAndProxyRequest(req, res, target);
689
+ await this.recordAndProxyRequest(req, res, target);
683
690
  } else {
684
691
  this.proxy.web(req, res, { target });
685
692
  }
686
693
  }
687
- // TODO: check if can handle streaming requests
688
- async bufferAndProxyRequest(req, res, target) {
689
- const chunks = [];
690
- req.on("data", (chunk) => {
691
- chunks.push(chunk);
692
- });
693
- try {
694
- await new Promise((resolve, reject) => {
695
- req.on("end", () => resolve());
696
- req.on("error", (err) => reject(err));
697
- setTimeout(
698
- () => reject(new Error("Request buffering timeout")),
699
- 3e4
700
- );
701
- });
702
- } catch (error) {
703
- console.error("Error buffering request:", error);
704
- }
705
- const body = Buffer.concat(chunks).toString("utf8");
706
- this.updateRequestBodySync(req, body);
707
- const targetUrl = new URL(target);
708
- const isHttps = targetUrl.protocol === "https:";
709
- const requestModule = isHttps ? https : http;
710
- const defaultPort = isHttps ? 443 : 80;
711
- const proxyReq = requestModule.request(
712
- {
713
- hostname: targetUrl.hostname,
714
- port: targetUrl.port || defaultPort,
715
- path: req.url,
716
- method: req.method,
717
- headers: req.headers
718
- },
719
- (proxyRes) => {
720
- this.addCorsHeaders(proxyRes, req);
721
- const responseChunks = [];
722
- proxyRes.on("data", (chunk) => {
723
- responseChunks.push(chunk);
724
- });
725
- proxyRes.on("end", async () => {
726
- const responseBody = Buffer.concat(responseChunks);
727
- const recorded = await this.recordResponseData(
728
- req,
729
- proxyRes,
730
- responseBody.toString("utf8")
731
- );
732
- const responseHeaders = {
733
- ...proxyRes.headers,
734
- ...this.getCorsHeaders(req)
735
- };
736
- res.writeHead(proxyRes.statusCode || 200, responseHeaders);
737
- res.end(responseBody);
738
- if (recorded) {
739
- console.log(`Recorded: ${req.method} ${req.url}`);
694
+ // Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
695
+ async recordAndProxyRequest(req, res, target) {
696
+ if (!this.currentSession) {
697
+ return;
698
+ }
699
+ const key = getReqID(req);
700
+ const recordingId = this.recordingIdCounter++;
701
+ const sequence = this.sequenceCounterByKey.get(key) || 0;
702
+ this.sequenceCounterByKey.set(key, sequence + 1);
703
+ const recordingPromise = new Promise((resolve) => {
704
+ (async () => {
705
+ try {
706
+ const chunks = [];
707
+ req.on("data", (chunk) => {
708
+ chunks.push(chunk);
709
+ });
710
+ try {
711
+ await new Promise((resolveBuffer, rejectBuffer) => {
712
+ req.on("end", () => resolveBuffer());
713
+ req.on("error", (err) => rejectBuffer(err));
714
+ setTimeout(
715
+ () => rejectBuffer(new Error("Request buffering timeout")),
716
+ 3e4
717
+ );
718
+ });
719
+ } catch (error) {
720
+ console.error("Error buffering request:", error);
740
721
  }
741
- });
742
- proxyRes.on("error", (err) => {
743
- console.error("Proxy response error:", err);
744
- if (!res.headersSent) {
722
+ const requestBody = Buffer.concat(chunks).toString("utf8");
723
+ const targetUrl = new URL(target);
724
+ const isHttps = targetUrl.protocol === "https:";
725
+ const requestModule = isHttps ? https : http;
726
+ const defaultPort = isHttps ? 443 : 80;
727
+ const proxyReq = requestModule.request(
728
+ {
729
+ hostname: targetUrl.hostname,
730
+ port: targetUrl.port || defaultPort,
731
+ path: req.url,
732
+ method: req.method,
733
+ headers: req.headers
734
+ },
735
+ (proxyRes) => {
736
+ this.addCorsHeaders(proxyRes, req);
737
+ const responseChunks = [];
738
+ proxyRes.on("data", (chunk) => {
739
+ responseChunks.push(chunk);
740
+ });
741
+ proxyRes.on("end", async () => {
742
+ try {
743
+ const responseBody = Buffer.concat(responseChunks);
744
+ const responseBodyStr = responseBody.toString("utf8");
745
+ const recording = {
746
+ request: {
747
+ method: req.method,
748
+ url: req.url,
749
+ headers: req.headers,
750
+ body: requestBody || null
751
+ },
752
+ response: {
753
+ statusCode: proxyRes.statusCode,
754
+ headers: proxyRes.headers,
755
+ body: responseBodyStr || null
756
+ },
757
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
758
+ key,
759
+ recordingId,
760
+ sequence
761
+ };
762
+ const responseHeaders = {
763
+ ...proxyRes.headers,
764
+ ...this.getCorsHeaders(req)
765
+ };
766
+ res.writeHead(proxyRes.statusCode || 200, responseHeaders);
767
+ res.end(responseBody);
768
+ console.log(
769
+ `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId}, sequence: ${sequence})`
770
+ );
771
+ resolve(recording);
772
+ } catch (error) {
773
+ console.error("Error completing recording:", error);
774
+ resolve(null);
775
+ }
776
+ });
777
+ proxyRes.on("error", (err) => {
778
+ console.error("Proxy response error:", err);
779
+ if (!res.headersSent) {
780
+ this.handleProxyError(err, req, res);
781
+ }
782
+ resolve(null);
783
+ });
784
+ }
785
+ );
786
+ proxyReq.on("error", (err) => {
745
787
  this.handleProxyError(err, req, res);
788
+ resolve(null);
789
+ });
790
+ if (chunks.length > 0) {
791
+ proxyReq.write(Buffer.concat(chunks));
746
792
  }
747
- });
748
- }
749
- );
750
- proxyReq.on("error", (err) => {
751
- this.handleProxyError(err, req, res);
793
+ proxyReq.end();
794
+ } catch (error) {
795
+ console.error("Error in recordAndProxyRequest:", error);
796
+ try {
797
+ this.handleProxyError(error, req, res);
798
+ } catch (error_) {
799
+ console.error("Failed to handle proxy error:", error_);
800
+ }
801
+ resolve(null);
802
+ }
803
+ })();
752
804
  });
753
- if (chunks.length > 0) {
754
- proxyReq.write(Buffer.concat(chunks));
755
- }
756
- proxyReq.end();
805
+ this.recordingPromises.push(recordingPromise);
806
+ await recordingPromise;
757
807
  }
758
808
  handleUpgrade(req, socket, head) {
759
809
  if (this.mode === Modes.replay) {