test-proxy-recorder 0.2.0 → 0.3.1

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
 
@@ -50,6 +50,7 @@ var HTTP_STATUS_OK = 200;
50
50
  var HTTP_STATUS_BAD_REQUEST = 400;
51
51
  var HTTP_STATUS_NOT_FOUND = 404;
52
52
  var CONTROL_ENDPOINT = "/__control";
53
+ var RECORDING_ID_HEADER = "x-test-rcrd-id";
53
54
 
54
55
  // src/types.ts
55
56
  var Modes = {
@@ -83,13 +84,23 @@ async function loadRecordingSession(filePath) {
83
84
  return JSON.parse(fileContent);
84
85
  }
85
86
  function processRecordings(recordings) {
86
- const keySequenceMap = /* @__PURE__ */ new Map();
87
- return recordings.map((recording) => {
87
+ const recordingsByKey = /* @__PURE__ */ new Map();
88
+ for (const recording of recordings) {
88
89
  const key = recording.key;
89
- const currentSeq = keySequenceMap.get(key) || 0;
90
- keySequenceMap.set(key, currentSeq + 1);
91
- return { ...recording, sequence: currentSeq };
92
- });
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;
93
104
  }
94
105
  async function saveRecordingSession(recordingsDir2, session) {
95
106
  const filePath = getRecordingPath(recordingsDir2, session.id);
@@ -152,19 +163,25 @@ var ProxyServer = class {
152
163
  recordingsDir;
153
164
  recordingIdCounter;
154
165
  // Unique ID for each recording entry
166
+ sequenceCounterByKey;
167
+ // Sequence counter per key (endpoint)
155
168
  replaySessions;
156
169
  // Track multiple concurrent replay sessions by recording ID
170
+ recordingPromises;
171
+ // Stack of promises that resolve to completed recordings
157
172
  constructor(targets2, recordingsDir2) {
158
173
  this.targets = targets2;
159
174
  this.currentTargetIndex = 0;
160
175
  this.mode = Modes.transparent;
161
176
  this.recordingId = null;
162
177
  this.recordingIdCounter = 0;
178
+ this.sequenceCounterByKey = /* @__PURE__ */ new Map();
163
179
  this.replayId = null;
164
180
  this.modeTimeout = null;
165
181
  this.currentSession = null;
166
182
  this.recordingsDir = recordingsDir2;
167
183
  this.replaySessions = /* @__PURE__ */ new Map();
184
+ this.recordingPromises = [];
168
185
  this.proxy = httpProxy.createProxyServer({
169
186
  secure: false,
170
187
  changeOrigin: true,
@@ -190,7 +207,7 @@ var ProxyServer = class {
190
207
  }
191
208
  setupProxyEventHandlers() {
192
209
  this.proxy.on("error", this.handleProxyError.bind(this));
193
- this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
210
+ this.proxy.on("proxyRes", this.addCorsHeaders.bind(this));
194
211
  }
195
212
  handleProxyError(err, req, res) {
196
213
  console.error("Proxy error:", err);
@@ -206,12 +223,6 @@ var ProxyServer = class {
206
223
  }
207
224
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
208
225
  }
209
- handleProxyResponse(proxyRes, req) {
210
- this.addCorsHeaders(proxyRes, req);
211
- if (this.mode === Modes.record && this.recordingId) {
212
- this.recordResponse(req, proxyRes);
213
- }
214
- }
215
226
  /**
216
227
  * Get CORS headers for a given request
217
228
  * @param req The incoming HTTP request
@@ -222,7 +233,7 @@ var ProxyServer = class {
222
233
  return {
223
234
  "access-control-allow-origin": origin || "*",
224
235
  "access-control-allow-credentials": "true",
225
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
236
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || `Origin, X-Requested-With, Content-Type, Accept, Authorization, ${RECORDING_ID_HEADER}`,
226
237
  "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
227
238
  "access-control-expose-headers": "*"
228
239
  };
@@ -236,9 +247,22 @@ var ProxyServer = class {
236
247
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
237
248
  return target;
238
249
  }
250
+ /**
251
+ * Extract recording ID from custom HTTP header
252
+ * Used for concurrent replay session routing, especially with Next.js
253
+ * @param req The incoming HTTP request
254
+ * @returns The recording ID from header, or null if not found
255
+ */
256
+ getRecordingIdFromHeader(req) {
257
+ const headerValue = req.headers[RECORDING_ID_HEADER];
258
+ if (!headerValue) {
259
+ return null;
260
+ }
261
+ return Array.isArray(headerValue) ? headerValue[0] : headerValue;
262
+ }
239
263
  /**
240
264
  * Extract recording ID from request cookie
241
- * Used for concurrent replay session routing
265
+ * Used for concurrent replay session routing (fallback method)
242
266
  * @param req The incoming HTTP request
243
267
  * @returns The recording ID from cookie, or null if not found
244
268
  */
@@ -250,6 +274,14 @@ var ProxyServer = class {
250
274
  const match = cookies.match(/proxy-recording-id=([^;]+)/);
251
275
  return match ? decodeURIComponent(match[1]) : null;
252
276
  }
277
+ /**
278
+ * Extract recording ID from request using custom header (preferred) or cookie (fallback)
279
+ * @param req The incoming HTTP request
280
+ * @returns The recording ID, or null if not found
281
+ */
282
+ getRecordingIdFromRequest(req) {
283
+ return this.getRecordingIdFromHeader(req) || this.getRecordingIdFromCookie(req);
284
+ }
253
285
  /**
254
286
  * Get or create a replay session state for a given recording ID
255
287
  * @param recordingId The recording ID to get/create session for
@@ -284,18 +316,20 @@ var ProxyServer = class {
284
316
  }
285
317
  return { mode, id, timeout };
286
318
  }
319
+ async parseControlRequest(req) {
320
+ if (req.method === "GET") {
321
+ return this.parseGetParams(req);
322
+ }
323
+ if (req.method === "POST") {
324
+ const body = await readRequestBody(req);
325
+ console.log(`MODE CHANGE (${req.method})`, body);
326
+ return JSON.parse(body);
327
+ }
328
+ throw new Error("Unsupported control method");
329
+ }
287
330
  async handleControlRequest(req, res) {
288
331
  try {
289
- let data;
290
- if (req.method === "GET") {
291
- data = this.parseGetParams(req);
292
- } else if (req.method === "POST") {
293
- const body = await readRequestBody(req);
294
- console.log(`MODE CHANGE (${req.method})`, body);
295
- data = JSON.parse(body);
296
- } else {
297
- return;
298
- }
332
+ const data = await this.parseControlRequest(req);
299
333
  const { mode, id, timeout: requestTimeout } = data;
300
334
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
301
335
  this.clearModeTimeout();
@@ -328,7 +362,7 @@ var ProxyServer = class {
328
362
  async switchMode(mode, id) {
329
363
  console.log(`Switching to ${mode.toUpperCase()} mode`);
330
364
  if (this.currentSession && this.mode === Modes.record) {
331
- await this.saveCurrentSession(true);
365
+ await this.saveCurrentSession();
332
366
  console.log("Session saved, continuing with mode switch");
333
367
  }
334
368
  switch (mode) {
@@ -368,6 +402,8 @@ var ProxyServer = class {
368
402
  this.recordingId = id;
369
403
  this.replayId = null;
370
404
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
405
+ this.recordingIdCounter = 0;
406
+ this.sequenceCounterByKey.clear();
371
407
  console.log(`Switched to record mode with ID: ${id}`);
372
408
  }
373
409
  async switchToReplayMode(id) {
@@ -387,169 +423,91 @@ var ProxyServer = class {
387
423
  setupModeTimeout(timeout) {
388
424
  this.modeTimeout = setTimeout(async () => {
389
425
  console.log("Timeout reached, switching back to transparent mode");
390
- await this.saveCurrentSession(true);
426
+ await this.saveCurrentSession();
391
427
  this.switchToTransparentMode();
392
428
  this.modeTimeout = null;
393
429
  }, timeout);
394
430
  }
395
- async saveCurrentSession(filterIncomplete = false) {
396
- if (!this.currentSession) {
431
+ async flushPendingRecordings() {
432
+ if (this.recordingPromises.length === 0) {
397
433
  return;
398
434
  }
399
- if (filterIncomplete) {
400
- const incompleteCount = this.currentSession.recordings.filter(
401
- (r) => !r.response
402
- ).length;
403
- if (incompleteCount > 0) {
404
- this.currentSession.recordings = this.currentSession.recordings.filter(
405
- (r) => r.response
406
- );
435
+ const results = await Promise.allSettled(this.recordingPromises);
436
+ if (this.currentSession) {
437
+ for (const result of results) {
438
+ if (result.status === "fulfilled" && result.value) {
439
+ this.currentSession.recordings.push(result.value);
440
+ }
407
441
  }
442
+ console.log(
443
+ `Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
444
+ );
408
445
  }
409
- console.log(
410
- `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
411
- );
412
- await saveRecordingSession(this.recordingsDir, this.currentSession);
446
+ this.recordingPromises = [];
413
447
  }
414
- saveRequestRecordSync(req, body) {
448
+ async saveCurrentSession() {
415
449
  if (!this.currentSession) {
416
450
  return;
417
451
  }
418
- const key = getReqID(req);
419
- const recordingId = this.recordingIdCounter++;
420
- req.__recordingId = recordingId;
421
- const record = {
422
- request: {
423
- method: req.method,
424
- url: req.url,
425
- headers: req.headers,
426
- body: body || null
427
- },
428
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
429
- key,
430
- recordingId
431
- };
432
- this.currentSession.recordings.push(record);
452
+ await this.flushPendingRecordings();
433
453
  console.log(
434
- // eslint-disable-next-line sonarjs/no-nested-template-literals
435
- `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})`
454
+ `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
436
455
  );
456
+ await saveRecordingSession(this.recordingsDir, this.currentSession);
437
457
  }
438
- updateRequestBodySync(req, body) {
439
- if (!this.currentSession) {
440
- return;
441
- }
442
- const recordingId = req.__recordingId;
443
- if (recordingId === void 0) {
444
- console.error(
445
- `updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
446
- );
447
- return;
448
- }
449
- const record = this.currentSession.recordings.find(
450
- (r) => r.recordingId === recordingId
451
- );
452
- if (!record) {
453
- console.error(
454
- `updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
455
- );
456
- return;
458
+ getRecordingIdOrError(req, res) {
459
+ const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
460
+ if (!recordingId) {
461
+ const corsHeaders = this.getCorsHeaders(req);
462
+ res.writeHead(HTTP_STATUS_BAD_REQUEST, {
463
+ "Content-Type": "application/json",
464
+ ...corsHeaders
465
+ });
466
+ res.end(JSON.stringify({ error: "No replay session active" }));
467
+ return null;
457
468
  }
458
- record.request.body = body || null;
459
- console.log(
460
- `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
461
- );
469
+ return recordingId;
462
470
  }
463
- async recordResponse(req, proxyRes) {
464
- if (!this.currentSession) {
465
- return;
466
- }
467
- const recordingId = req.__recordingId;
468
- if (recordingId === void 0) {
469
- console.error(
470
- `recordResponse: No recording ID found on request ${req.method} ${req.url}`
471
- );
472
- return;
473
- }
474
- const record = this.currentSession.recordings.find(
475
- (r) => r.recordingId === recordingId
476
- );
477
- if (!record) {
478
- console.error(
479
- `recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
480
- );
481
- return;
471
+ async ensureSessionLoaded(recordingId, filePath) {
472
+ const sessionState = this.getOrCreateReplaySession(recordingId);
473
+ if (!sessionState.loadedSession) {
474
+ sessionState.loadedSession = await loadRecordingSession(filePath);
475
+ console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
482
476
  }
483
- const chunks = [];
484
- proxyRes.on("data", (chunk) => {
485
- chunks.push(chunk);
486
- });
487
- proxyRes.on("end", async () => {
488
- const body = Buffer.concat(chunks).toString("utf8");
489
- record.response = {
490
- statusCode: proxyRes.statusCode,
491
- headers: proxyRes.headers,
492
- body: body || null
493
- };
494
- console.log(
495
- `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
496
- );
497
- });
477
+ return sessionState;
498
478
  }
499
- async recordResponseData(req, proxyRes, body) {
500
- if (!this.currentSession) {
501
- return false;
479
+ getServedTracker(sessionState, key) {
480
+ if (!sessionState.servedRecordingIdsByKey.has(key)) {
481
+ sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
502
482
  }
503
- const recordingId = req.__recordingId;
504
- if (recordingId === void 0) {
505
- console.error(
506
- `recordResponseData: No recording ID found on request ${req.method} ${req.url}`
507
- );
508
- return false;
483
+ return sessionState.servedRecordingIdsByKey.get(key);
484
+ }
485
+ selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
486
+ for (const rec of recordsWithKey) {
487
+ if (!servedForThisKey.has(rec.recordingId)) {
488
+ return rec;
489
+ }
509
490
  }
510
- const record = this.currentSession.recordings.find(
511
- (r) => r.recordingId === recordingId
512
- );
513
- if (!record) {
514
- console.error(
515
- `recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
491
+ if (recordsWithKey.length > 0) {
492
+ console.log(
493
+ `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
516
494
  );
517
- return false;
495
+ return recordsWithKey[recordsWithKey.length - 1];
518
496
  }
519
- record.response = {
520
- statusCode: proxyRes.statusCode,
521
- headers: proxyRes.headers,
522
- body: body || null
523
- };
524
- console.log(
525
- `recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
526
- );
527
- return true;
497
+ return null;
528
498
  }
529
499
  async handleReplayRequest(req, res) {
530
- const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
531
- if (!recordingId) {
532
- const corsHeaders = this.getCorsHeaders(req);
533
- res.writeHead(HTTP_STATUS_BAD_REQUEST, {
534
- "Content-Type": "application/json",
535
- ...corsHeaders
536
- });
537
- res.end(JSON.stringify({ error: "No replay session active" }));
538
- return;
539
- }
500
+ const recordingId = this.getRecordingIdOrError(req, res);
501
+ if (!recordingId) return;
540
502
  const key = getReqID(req);
541
503
  const filePath = getRecordingPath(this.recordingsDir, recordingId);
542
504
  try {
543
- const sessionState = this.getOrCreateReplaySession(recordingId);
544
- if (!sessionState.loadedSession) {
545
- sessionState.loadedSession = await loadRecordingSession(filePath);
546
- console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
547
- }
505
+ const sessionState = await this.ensureSessionLoaded(
506
+ recordingId,
507
+ filePath
508
+ );
548
509
  const session = sessionState.loadedSession;
549
- if (!sessionState.servedRecordingIdsByKey.has(key)) {
550
- sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
551
- }
552
- const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
510
+ const servedForThisKey = this.getServedTracker(sessionState, key);
553
511
  const host = req.headers.host || "unknown";
554
512
  const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
555
513
  const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
@@ -578,30 +536,23 @@ var ProxyServer = class {
578
536
  }
579
537
  const requestCount = servedForThisKey.size + 1;
580
538
  console.log(
581
- `[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
539
+ `[replay request #${requestCount}] ${req.method} ${req.url} (key: ${key}, session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
582
540
  );
583
- let record;
584
- for (const rec of recordsWithKey) {
585
- if (!servedForThisKey.has(rec.recordingId)) {
586
- record = rec;
587
- break;
588
- }
589
- }
590
- if (!record) {
591
- console.log(
592
- `[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
541
+ const record = this.selectReplayRecord(
542
+ recordsWithKey,
543
+ servedForThisKey,
544
+ key,
545
+ recordingId
546
+ );
547
+ if (!record || !record.response) {
548
+ throw new Error(
549
+ `No response recorded for this request: ${req.method} ${host}${req.url}`
593
550
  );
594
- record = recordsWithKey[recordsWithKey.length - 1];
595
551
  }
596
552
  servedForThisKey.add(record.recordingId);
597
553
  console.log(
598
554
  `[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
599
555
  );
600
- if (!record.response) {
601
- throw new Error(
602
- `No response recorded for this request: ${req.method} ${host}${req.url}`
603
- );
604
- }
605
556
  const { statusCode, headers, body } = record.response;
606
557
  const responseHeaders = {
607
558
  ...headers,
@@ -656,82 +607,124 @@ var ProxyServer = class {
656
607
  const target = this.getTarget();
657
608
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
658
609
  if (this.mode === Modes.record) {
659
- this.saveRequestRecordSync(req, null);
660
- await this.bufferAndProxyRequest(req, res, target);
610
+ await this.recordAndProxyRequest(req, res, target);
661
611
  } else {
662
612
  this.proxy.web(req, res, { target });
663
613
  }
664
614
  }
665
- // TODO: check if can handle streaming requests
666
- async bufferAndProxyRequest(req, res, target) {
667
- const chunks = [];
668
- req.on("data", (chunk) => {
669
- chunks.push(chunk);
670
- });
671
- try {
672
- await new Promise((resolve, reject) => {
673
- req.on("end", () => resolve());
674
- req.on("error", (err) => reject(err));
675
- setTimeout(
676
- () => reject(new Error("Request buffering timeout")),
677
- 3e4
678
- );
679
- });
680
- } catch (error) {
681
- console.error("Error buffering request:", error);
615
+ // Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
616
+ async recordAndProxyRequest(req, res, target) {
617
+ if (!this.currentSession) {
618
+ return;
682
619
  }
683
- const body = Buffer.concat(chunks).toString("utf8");
684
- this.updateRequestBodySync(req, body);
685
- const targetUrl = new URL(target);
686
- const isHttps = targetUrl.protocol === "https:";
687
- const requestModule = isHttps ? https : http;
688
- const defaultPort = isHttps ? 443 : 80;
689
- const proxyReq = requestModule.request(
690
- {
691
- hostname: targetUrl.hostname,
692
- port: targetUrl.port || defaultPort,
693
- path: req.url,
694
- method: req.method,
695
- headers: req.headers
696
- },
697
- (proxyRes) => {
698
- this.addCorsHeaders(proxyRes, req);
699
- const responseChunks = [];
700
- proxyRes.on("data", (chunk) => {
701
- responseChunks.push(chunk);
702
- });
703
- proxyRes.on("end", async () => {
704
- const responseBody = Buffer.concat(responseChunks);
705
- const recorded = await this.recordResponseData(
706
- req,
707
- proxyRes,
708
- responseBody.toString("utf8")
709
- );
710
- const responseHeaders = {
711
- ...proxyRes.headers,
712
- ...this.getCorsHeaders(req)
713
- };
714
- res.writeHead(proxyRes.statusCode || 200, responseHeaders);
715
- res.end(responseBody);
716
- if (recorded) {
717
- console.log(`Recorded: ${req.method} ${req.url}`);
620
+ const key = getReqID(req);
621
+ const recordingId = this.recordingIdCounter++;
622
+ const sequence = this.sequenceCounterByKey.get(key) || 0;
623
+ this.sequenceCounterByKey.set(key, sequence + 1);
624
+ const recordingPromise = new Promise((resolve) => {
625
+ (async () => {
626
+ try {
627
+ const chunks = [];
628
+ req.on("data", (chunk) => {
629
+ chunks.push(chunk);
630
+ });
631
+ try {
632
+ await new Promise((resolveBuffer, rejectBuffer) => {
633
+ req.on("end", () => resolveBuffer());
634
+ req.on("error", (err) => rejectBuffer(err));
635
+ setTimeout(
636
+ () => rejectBuffer(new Error("Request buffering timeout")),
637
+ 3e4
638
+ );
639
+ });
640
+ } catch (error) {
641
+ console.error("Error buffering request:", error);
718
642
  }
719
- });
720
- proxyRes.on("error", (err) => {
721
- console.error("Proxy response error:", err);
722
- if (!res.headersSent) {
643
+ const requestBody = Buffer.concat(chunks).toString("utf8");
644
+ const targetUrl = new URL(target);
645
+ const isHttps = targetUrl.protocol === "https:";
646
+ const requestModule = isHttps ? https : http;
647
+ const defaultPort = isHttps ? 443 : 80;
648
+ const proxyReq = requestModule.request(
649
+ {
650
+ hostname: targetUrl.hostname,
651
+ port: targetUrl.port || defaultPort,
652
+ path: req.url,
653
+ method: req.method,
654
+ headers: req.headers
655
+ },
656
+ (proxyRes) => {
657
+ this.addCorsHeaders(proxyRes, req);
658
+ const responseChunks = [];
659
+ proxyRes.on("data", (chunk) => {
660
+ responseChunks.push(chunk);
661
+ });
662
+ proxyRes.on("end", async () => {
663
+ try {
664
+ const responseBody = Buffer.concat(responseChunks);
665
+ const responseBodyStr = responseBody.toString("utf8");
666
+ const recording = {
667
+ request: {
668
+ method: req.method,
669
+ url: req.url,
670
+ headers: req.headers,
671
+ body: requestBody || null
672
+ },
673
+ response: {
674
+ statusCode: proxyRes.statusCode,
675
+ headers: proxyRes.headers,
676
+ body: responseBodyStr || null
677
+ },
678
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
679
+ key,
680
+ recordingId,
681
+ sequence
682
+ };
683
+ const responseHeaders = {
684
+ ...proxyRes.headers,
685
+ ...this.getCorsHeaders(req)
686
+ };
687
+ res.writeHead(proxyRes.statusCode || 200, responseHeaders);
688
+ res.end(responseBody);
689
+ console.log(
690
+ `Recorded: ${req.method} ${req.url} (recordingId: ${recordingId}, sequence: ${sequence})`
691
+ );
692
+ resolve(recording);
693
+ } catch (error) {
694
+ console.error("Error completing recording:", error);
695
+ resolve(null);
696
+ }
697
+ });
698
+ proxyRes.on("error", (err) => {
699
+ console.error("Proxy response error:", err);
700
+ if (!res.headersSent) {
701
+ this.handleProxyError(err, req, res);
702
+ }
703
+ resolve(null);
704
+ });
705
+ }
706
+ );
707
+ proxyReq.on("error", (err) => {
723
708
  this.handleProxyError(err, req, res);
709
+ resolve(null);
710
+ });
711
+ if (chunks.length > 0) {
712
+ proxyReq.write(Buffer.concat(chunks));
724
713
  }
725
- });
726
- }
727
- );
728
- proxyReq.on("error", (err) => {
729
- this.handleProxyError(err, req, res);
714
+ proxyReq.end();
715
+ } catch (error) {
716
+ console.error("Error in recordAndProxyRequest:", error);
717
+ try {
718
+ this.handleProxyError(error, req, res);
719
+ } catch (error_) {
720
+ console.error("Failed to handle proxy error:", error_);
721
+ }
722
+ resolve(null);
723
+ }
724
+ })();
730
725
  });
731
- if (chunks.length > 0) {
732
- proxyReq.write(Buffer.concat(chunks));
733
- }
734
- proxyReq.end();
726
+ this.recordingPromises.push(recordingPromise);
727
+ await recordingPromise;
735
728
  }
736
729
  handleUpgrade(req, socket, head) {
737
730
  if (this.mode === Modes.replay) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -15,6 +15,11 @@
15
15
  "require": "./dist/playwright/index.cjs",
16
16
  "types": "./dist/playwright/index.d.ts",
17
17
  "default": "./dist/playwright/index.mjs"
18
+ },
19
+ "./nextjs": {
20
+ "require": "./dist/nextjs/index.cjs",
21
+ "types": "./dist/nextjs/index.d.ts",
22
+ "default": "./dist/nextjs/index.mjs"
18
23
  }
19
24
  },
20
25
  "bin": {