test-proxy-recorder 0.1.5 → 0.1.7

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/index.cjs CHANGED
@@ -138,14 +138,16 @@ var ProxyServer = class {
138
138
  this.proxy.on("error", this.handleProxyError.bind(this));
139
139
  this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
140
140
  }
141
- handleProxyError(err, _req, res) {
141
+ handleProxyError(err, req, res) {
142
142
  console.error("Proxy error:", err);
143
143
  if (!(res instanceof http__default.default.ServerResponse)) {
144
144
  return;
145
145
  }
146
146
  if (!res.headersSent) {
147
+ const corsHeaders = this.getCorsHeaders(req);
147
148
  res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
148
- "Content-Type": "application/json"
149
+ "Content-Type": "application/json",
150
+ ...corsHeaders
149
151
  });
150
152
  }
151
153
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
@@ -156,24 +158,51 @@ var ProxyServer = class {
156
158
  this.recordResponse(req, proxyRes);
157
159
  }
158
160
  }
159
- addCorsHeaders(proxyRes, req) {
161
+ /**
162
+ * Get CORS headers for a given request
163
+ * @param req The incoming HTTP request
164
+ * @returns An object containing CORS headers
165
+ */
166
+ getCorsHeaders(req) {
160
167
  const origin = req.headers.origin;
161
- proxyRes.headers["access-control-allow-origin"] = origin || "*";
162
- proxyRes.headers["access-control-allow-credentials"] = "true";
163
- proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
164
- proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
165
- proxyRes.headers["access-control-expose-headers"] = "*";
168
+ return {
169
+ "access-control-allow-origin": origin || "*",
170
+ "access-control-allow-credentials": "true",
171
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
172
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
173
+ "access-control-expose-headers": "*"
174
+ };
175
+ }
176
+ addCorsHeaders(proxyRes, req) {
177
+ const corsHeaders = this.getCorsHeaders(req);
178
+ Object.assign(proxyRes.headers, corsHeaders);
166
179
  }
167
180
  getTarget() {
168
181
  const target = this.targets[this.currentTargetIndex];
169
182
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
170
183
  return target;
171
184
  }
185
+ parseGetParams(req) {
186
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
187
+ const mode = url.searchParams.get("mode");
188
+ const id = url.searchParams.get("id") || void 0;
189
+ const timeoutParam = url.searchParams.get("timeout");
190
+ const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
191
+ if (!mode) {
192
+ throw new Error("Mode parameter is required");
193
+ }
194
+ return { mode, id, timeout };
195
+ }
172
196
  async handleControlRequest(req, res) {
173
197
  try {
174
- const body = await readRequestBody(req);
175
- console.log("MODE CHANGE", body);
176
- const data = JSON.parse(body);
198
+ let data;
199
+ if (req.method === "GET") {
200
+ data = this.parseGetParams(req);
201
+ } else {
202
+ const body = await readRequestBody(req);
203
+ console.log("MODE CHANGE (POST)", body);
204
+ data = JSON.parse(body);
205
+ }
177
206
  const { mode, id, timeout: requestTimeout } = data;
178
207
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
179
208
  this.clearModeTimeout();
@@ -201,7 +230,7 @@ var ProxyServer = class {
201
230
  async switchMode(mode, id) {
202
231
  if (this.currentSession) {
203
232
  console.log("Switching mode, saving current session first");
204
- await this.saveCurrentSession();
233
+ await this.saveCurrentSession(true);
205
234
  console.log("Session saved, continuing with mode switch");
206
235
  }
207
236
  switch (mode) {
@@ -256,33 +285,41 @@ var ProxyServer = class {
256
285
  if (timeout && timeout > 0) {
257
286
  this.modeTimeout = setTimeout(async () => {
258
287
  console.log("Timeout reached, switching back to transparent mode");
259
- await this.saveCurrentSession();
288
+ await this.saveCurrentSession(true);
260
289
  this.switchToTransparentMode();
261
290
  this.modeTimeout = null;
262
291
  }, timeout);
263
292
  }
264
293
  }
265
- async saveCurrentSession() {
294
+ async saveCurrentSession(filterIncomplete = false) {
266
295
  if (!this.currentSession) {
267
296
  console.log("No current session to save");
268
297
  return;
269
298
  }
270
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
271
- console.log("Session has no recordings, skipping save");
272
- return;
299
+ if (filterIncomplete) {
300
+ const incompleteCount = this.currentSession.recordings.filter(
301
+ (r) => !r.response
302
+ ).length;
303
+ if (incompleteCount > 0) {
304
+ console.log(
305
+ `Removing ${incompleteCount} incomplete recording(s) without responses`
306
+ );
307
+ this.currentSession.recordings = this.currentSession.recordings.filter(
308
+ (r) => r.response
309
+ );
310
+ }
273
311
  }
274
312
  console.log(
275
313
  `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
276
314
  );
277
315
  await saveRecordingSession(this.recordingsDir, this.currentSession);
278
316
  }
279
- async saveRequestRecord(req, body) {
317
+ saveRequestRecordSync(req, body) {
280
318
  if (!this.currentSession) {
319
+ console.log("saveRequestRecordSync: No current session");
281
320
  return;
282
321
  }
283
322
  const key = getReqID(req);
284
- const currentSequence = this.requestSequenceMap.get(key) || 0;
285
- this.requestSequenceMap.set(key, currentSequence + 1);
286
323
  const record = {
287
324
  request: {
288
325
  method: req.method,
@@ -292,9 +329,34 @@ var ProxyServer = class {
292
329
  },
293
330
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
294
331
  key,
295
- sequence: currentSequence
332
+ sequence: -1
333
+ // Temporary, will be set when response arrives
296
334
  };
297
335
  this.currentSession.recordings.push(record);
336
+ console.log(
337
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
338
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
339
+ );
340
+ }
341
+ updateRequestBodySync(req, body) {
342
+ if (!this.currentSession) {
343
+ console.log("updateRequestBodySync: No current session");
344
+ return;
345
+ }
346
+ const key = getReqID(req);
347
+ const record = this.currentSession.recordings.findLast(
348
+ (r) => r.key === key && !r.response
349
+ );
350
+ if (!record) {
351
+ console.error(
352
+ `updateRequestBodySync: Could not find request record for ${req.method} ${req.url}`
353
+ );
354
+ return;
355
+ }
356
+ record.request.body = body || null;
357
+ console.log(
358
+ `updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
359
+ );
298
360
  }
299
361
  async recordResponse(req, proxyRes) {
300
362
  if (!this.currentSession) {
@@ -319,59 +381,115 @@ var ProxyServer = class {
319
381
  headers: proxyRes.headers,
320
382
  body: body || null
321
383
  };
322
- await this.saveCurrentSession();
323
384
  console.log(`Recorded: ${req.method} ${req.url}`);
324
385
  });
325
386
  }
387
+ async recordResponseData(req, proxyRes, body) {
388
+ if (!this.currentSession) {
389
+ console.log("recordResponseData: No current session");
390
+ return false;
391
+ }
392
+ const key = getReqID(req);
393
+ const record = this.currentSession.recordings.findLast(
394
+ (r) => r.key === key && !r.response
395
+ );
396
+ if (!record) {
397
+ const host = req.headers.host || "unknown";
398
+ const recordsWithKey = this.currentSession.recordings.filter(
399
+ (r) => r.key === key
400
+ );
401
+ console.error(
402
+ `Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
403
+ );
404
+ console.error(
405
+ ` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
406
+ );
407
+ console.error(
408
+ ` Records with key:`,
409
+ recordsWithKey.map((r) => ({
410
+ seq: r.sequence,
411
+ hasResponse: !!r.response
412
+ }))
413
+ );
414
+ return false;
415
+ }
416
+ record.response = {
417
+ statusCode: proxyRes.statusCode,
418
+ headers: proxyRes.headers,
419
+ body: body || null
420
+ };
421
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
422
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
423
+ record.sequence = currentSequence;
424
+ this.requestSequenceMap.set(key, currentSequence + 1);
425
+ console.log(
426
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
427
+ );
428
+ return true;
429
+ }
326
430
  async handleReplayRequest(req, res) {
327
431
  const key = getReqID(req);
328
432
  const filePath = getRecordingPath(this.recordingsDir, this.replayId);
329
433
  try {
330
434
  const session = await loadRecordingSession(filePath);
331
- const currentSequence = this.replaySequenceMap.get(key) || 0;
332
- const record = session.recordings.find(
333
- (r) => r.key === key && r.sequence === currentSequence
334
- );
335
- if (!record) {
435
+ const host = req.headers.host || "unknown";
436
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
437
+ if (recordsWithKey.length === 0) {
336
438
  throw new Error(
337
- `No recording found for ${key} with sequence ${currentSequence}`
439
+ `No recording found for ${key} at ${req.method} ${host}${req.url}`
338
440
  );
339
441
  }
442
+ const usageCount = this.replaySequenceMap.get(key) || 0;
443
+ let record;
444
+ if (recordsWithKey.length > 1) {
445
+ record = recordsWithKey[recordsWithKey.length - 1];
446
+ } else {
447
+ const recordIndex = usageCount % recordsWithKey.length;
448
+ record = recordsWithKey[recordIndex];
449
+ }
450
+ console.log(
451
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
452
+ );
453
+ this.replaySequenceMap.set(key, usageCount + 1);
340
454
  if (!record.response) {
341
- throw new Error("No response recorded for this request");
455
+ throw new Error(
456
+ `No response recorded for this request: ${req.method} ${host}${req.url}`
457
+ );
342
458
  }
343
- this.replaySequenceMap.set(key, currentSequence + 1);
344
459
  const { statusCode, headers, body } = record.response;
345
- const origin = req.headers.origin;
346
460
  const responseHeaders = {
347
461
  ...headers,
348
- "access-control-allow-origin": origin || "*",
349
- "access-control-allow-credentials": "true"
462
+ ...this.getCorsHeaders(req)
350
463
  };
351
464
  res.writeHead(statusCode, responseHeaders);
352
465
  res.end(body);
353
- console.log(
354
- `Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
355
- );
356
466
  } catch (error) {
357
- this.handleReplayError(res, error, key, filePath);
467
+ this.handleReplayError(req, res, error, key, filePath);
358
468
  }
359
469
  }
360
- handleReplayError(res, err, key, filePath) {
470
+ handleReplayError(req, res, err, key, filePath) {
361
471
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
362
472
  console.error("Replay error:", err);
363
- sendJsonResponse(res, HTTP_STATUS_NOT_FOUND, {
364
- error: isFileNotFound ? "Recording file not found" : "Recording not found",
365
- message: err instanceof Error ? err.message : "Unknown error",
366
- key,
367
- filePath
473
+ const corsHeaders = this.getCorsHeaders(req);
474
+ res.writeHead(HTTP_STATUS_NOT_FOUND, {
475
+ "Content-Type": "application/json",
476
+ ...corsHeaders
368
477
  });
478
+ res.end(
479
+ JSON.stringify({
480
+ error: isFileNotFound ? "Recording file not found" : "Recording not found",
481
+ message: err instanceof Error ? err.message : "Unknown error",
482
+ key,
483
+ filePath
484
+ })
485
+ );
369
486
  }
370
487
  async handleRequest(req, res) {
371
488
  if (req.method === "OPTIONS") {
372
489
  return this.handleCorsPreflightRequest(req, res);
373
490
  }
374
- if (req.url === CONTROL_ENDPOINT) {
491
+ const urlPath = req.url?.split("?")[0] || "";
492
+ if (urlPath === CONTROL_ENDPOINT) {
375
493
  return this.handleControlRequest(req, res);
376
494
  }
377
495
  if (this.mode === Modes.replay) {
@@ -380,12 +498,9 @@ var ProxyServer = class {
380
498
  await this.handleProxyRequest(req, res);
381
499
  }
382
500
  handleCorsPreflightRequest(req, res) {
383
- const origin = req.headers.origin;
501
+ const corsHeaders = this.getCorsHeaders(req);
384
502
  res.writeHead(HTTP_STATUS_OK, {
385
- "Access-Control-Allow-Origin": origin || "*",
386
- "Access-Control-Allow-Credentials": "true",
387
- "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
388
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
503
+ ...corsHeaders,
389
504
  "Access-Control-Max-Age": "86400"
390
505
  // 24 hours
391
506
  });
@@ -395,6 +510,7 @@ var ProxyServer = class {
395
510
  const target = this.getTarget();
396
511
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
397
512
  if (this.mode === Modes.record) {
513
+ this.saveRequestRecordSync(req, null);
398
514
  await this.bufferAndProxyRequest(req, res, target);
399
515
  } else {
400
516
  this.proxy.web(req, res, { target });
@@ -405,11 +521,20 @@ var ProxyServer = class {
405
521
  req.on("data", (chunk) => {
406
522
  chunks.push(chunk);
407
523
  });
408
- await new Promise((resolve) => {
409
- req.on("end", () => resolve());
410
- });
524
+ try {
525
+ await new Promise((resolve, reject) => {
526
+ req.on("end", () => resolve());
527
+ req.on("error", (err) => reject(err));
528
+ setTimeout(
529
+ () => reject(new Error("Request buffering timeout")),
530
+ 3e4
531
+ );
532
+ });
533
+ } catch (error) {
534
+ console.error("Error buffering request:", error);
535
+ }
411
536
  const body = Buffer.concat(chunks).toString("utf8");
412
- await this.saveRequestRecord(req, body);
537
+ this.updateRequestBodySync(req, body);
413
538
  const targetUrl = new URL(target);
414
539
  const isHttps = targetUrl.protocol === "https:";
415
540
  const requestModule = isHttps ? https__default.default : http__default.default;
@@ -424,9 +549,33 @@ var ProxyServer = class {
424
549
  },
425
550
  (proxyRes) => {
426
551
  this.addCorsHeaders(proxyRes, req);
427
- this.recordResponse(req, proxyRes);
428
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
429
- proxyRes.pipe(res);
552
+ const responseChunks = [];
553
+ proxyRes.on("data", (chunk) => {
554
+ responseChunks.push(chunk);
555
+ });
556
+ proxyRes.on("end", async () => {
557
+ const responseBody = Buffer.concat(responseChunks);
558
+ const recorded = await this.recordResponseData(
559
+ req,
560
+ proxyRes,
561
+ responseBody.toString("utf8")
562
+ );
563
+ const responseHeaders = {
564
+ ...proxyRes.headers,
565
+ ...this.getCorsHeaders(req)
566
+ };
567
+ res.writeHead(proxyRes.statusCode || 200, responseHeaders);
568
+ res.end(responseBody);
569
+ if (recorded) {
570
+ console.log(`Recorded: ${req.method} ${req.url}`);
571
+ }
572
+ });
573
+ proxyRes.on("error", (err) => {
574
+ console.error("Proxy response error:", err);
575
+ if (!res.headersSent) {
576
+ this.handleProxyError(err, req, res);
577
+ }
578
+ });
430
579
  }
431
580
  );
432
581
  proxyReq.on("error", (err) => {
@@ -478,9 +627,6 @@ var ProxyServer = class {
478
627
  if (backendWs.readyState === ws.WebSocket.OPEN) {
479
628
  backendWs.send(message);
480
629
  }
481
- this.saveCurrentSession().catch((error) => {
482
- console.error("Failed to save WebSocket recording:", error);
483
- });
484
630
  });
485
631
  backendWs.on("message", (data) => {
486
632
  const message = data.toString();
@@ -492,9 +638,6 @@ var ProxyServer = class {
492
638
  if (clientWs.readyState === ws.WebSocket.OPEN) {
493
639
  clientWs.send(message);
494
640
  }
495
- this.saveCurrentSession().catch((error) => {
496
- console.error("Failed to save WebSocket recording:", error);
497
- });
498
641
  });
499
642
  clientWs.on("error", (err) => {
500
643
  console.error("Client WebSocket error:", err);
package/dist/index.d.cts CHANGED
@@ -20,8 +20,15 @@ declare class ProxyServer {
20
20
  private setupProxyEventHandlers;
21
21
  private handleProxyError;
22
22
  private handleProxyResponse;
23
+ /**
24
+ * Get CORS headers for a given request
25
+ * @param req The incoming HTTP request
26
+ * @returns An object containing CORS headers
27
+ */
28
+ private getCorsHeaders;
23
29
  private addCorsHeaders;
24
30
  private getTarget;
31
+ private parseGetParams;
25
32
  private handleControlRequest;
26
33
  private clearModeTimeout;
27
34
  private switchMode;
@@ -30,8 +37,10 @@ declare class ProxyServer {
30
37
  private switchToReplayMode;
31
38
  private setupModeTimeout;
32
39
  private saveCurrentSession;
33
- private saveRequestRecord;
40
+ private saveRequestRecordSync;
41
+ private updateRequestBodySync;
34
42
  private recordResponse;
43
+ private recordResponseData;
35
44
  private handleReplayRequest;
36
45
  private handleReplayError;
37
46
  private handleRequest;
package/dist/index.d.ts CHANGED
@@ -20,8 +20,15 @@ declare class ProxyServer {
20
20
  private setupProxyEventHandlers;
21
21
  private handleProxyError;
22
22
  private handleProxyResponse;
23
+ /**
24
+ * Get CORS headers for a given request
25
+ * @param req The incoming HTTP request
26
+ * @returns An object containing CORS headers
27
+ */
28
+ private getCorsHeaders;
23
29
  private addCorsHeaders;
24
30
  private getTarget;
31
+ private parseGetParams;
25
32
  private handleControlRequest;
26
33
  private clearModeTimeout;
27
34
  private switchMode;
@@ -30,8 +37,10 @@ declare class ProxyServer {
30
37
  private switchToReplayMode;
31
38
  private setupModeTimeout;
32
39
  private saveCurrentSession;
33
- private saveRequestRecord;
40
+ private saveRequestRecordSync;
41
+ private updateRequestBodySync;
34
42
  private recordResponse;
43
+ private recordResponseData;
35
44
  private handleReplayRequest;
36
45
  private handleReplayError;
37
46
  private handleRequest;