test-proxy-recorder 0.3.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/README.md +4 -4
- package/dist/index.cjs +214 -243
- package/dist/index.d.cts +9 -6
- package/dist/index.d.ts +9 -6
- package/dist/index.mjs +214 -243
- package/dist/playwright/index.cjs +1 -1
- package/dist/playwright/index.mjs +1 -1
- package/dist/proxy.js +215 -244
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -14,13 +14,14 @@ declare class ProxyServer {
|
|
|
14
14
|
private currentSession;
|
|
15
15
|
private recordingsDir;
|
|
16
16
|
private recordingIdCounter;
|
|
17
|
+
private sequenceCounterByKey;
|
|
17
18
|
private replaySessions;
|
|
19
|
+
private recordingPromises;
|
|
18
20
|
constructor(targets: string[], recordingsDir: string);
|
|
19
21
|
init(): Promise<void>;
|
|
20
22
|
listen(port: number): http.Server;
|
|
21
23
|
private setupProxyEventHandlers;
|
|
22
24
|
private handleProxyError;
|
|
23
|
-
private handleProxyResponse;
|
|
24
25
|
/**
|
|
25
26
|
* Get CORS headers for a given request
|
|
26
27
|
* @param req The incoming HTTP request
|
|
@@ -56,6 +57,7 @@ declare class ProxyServer {
|
|
|
56
57
|
*/
|
|
57
58
|
private getOrCreateReplaySession;
|
|
58
59
|
private parseGetParams;
|
|
60
|
+
private parseControlRequest;
|
|
59
61
|
private handleControlRequest;
|
|
60
62
|
private clearModeTimeout;
|
|
61
63
|
private switchMode;
|
|
@@ -63,17 +65,18 @@ declare class ProxyServer {
|
|
|
63
65
|
private switchToRecordMode;
|
|
64
66
|
private switchToReplayMode;
|
|
65
67
|
private setupModeTimeout;
|
|
68
|
+
private flushPendingRecordings;
|
|
66
69
|
private saveCurrentSession;
|
|
67
|
-
private
|
|
68
|
-
private
|
|
69
|
-
private
|
|
70
|
-
private
|
|
70
|
+
private getRecordingIdOrError;
|
|
71
|
+
private ensureSessionLoaded;
|
|
72
|
+
private getServedTracker;
|
|
73
|
+
private selectReplayRecord;
|
|
71
74
|
private handleReplayRequest;
|
|
72
75
|
private handleReplayError;
|
|
73
76
|
private handleRequest;
|
|
74
77
|
private handleCorsPreflightRequest;
|
|
75
78
|
private handleProxyRequest;
|
|
76
|
-
private
|
|
79
|
+
private recordAndProxyRequest;
|
|
77
80
|
private handleUpgrade;
|
|
78
81
|
private handleRecordWebSocket;
|
|
79
82
|
private handleReplayWebSocket;
|
package/dist/index.mjs
CHANGED
|
@@ -48,13 +48,23 @@ async function loadRecordingSession(filePath) {
|
|
|
48
48
|
return JSON.parse(fileContent);
|
|
49
49
|
}
|
|
50
50
|
function processRecordings(recordings) {
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
const recordingsByKey = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const recording of recordings) {
|
|
53
53
|
const key = recording.key;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if (!recordingsByKey.has(key)) {
|
|
55
|
+
recordingsByKey.set(key, []);
|
|
56
|
+
}
|
|
57
|
+
recordingsByKey.get(key).push(recording);
|
|
58
|
+
}
|
|
59
|
+
const processedRecordings = [];
|
|
60
|
+
for (const [_key, keyRecordings] of recordingsByKey) {
|
|
61
|
+
keyRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
62
|
+
keyRecordings.forEach((recording, index) => {
|
|
63
|
+
processedRecordings.push({ ...recording, sequence: index });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
67
|
+
return processedRecordings;
|
|
58
68
|
}
|
|
59
69
|
async function saveRecordingSession(recordingsDir, session) {
|
|
60
70
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
@@ -117,19 +127,25 @@ var ProxyServer = class {
|
|
|
117
127
|
recordingsDir;
|
|
118
128
|
recordingIdCounter;
|
|
119
129
|
// Unique ID for each recording entry
|
|
130
|
+
sequenceCounterByKey;
|
|
131
|
+
// Sequence counter per key (endpoint)
|
|
120
132
|
replaySessions;
|
|
121
133
|
// Track multiple concurrent replay sessions by recording ID
|
|
134
|
+
recordingPromises;
|
|
135
|
+
// Stack of promises that resolve to completed recordings
|
|
122
136
|
constructor(targets, recordingsDir) {
|
|
123
137
|
this.targets = targets;
|
|
124
138
|
this.currentTargetIndex = 0;
|
|
125
139
|
this.mode = Modes.transparent;
|
|
126
140
|
this.recordingId = null;
|
|
127
141
|
this.recordingIdCounter = 0;
|
|
142
|
+
this.sequenceCounterByKey = /* @__PURE__ */ new Map();
|
|
128
143
|
this.replayId = null;
|
|
129
144
|
this.modeTimeout = null;
|
|
130
145
|
this.currentSession = null;
|
|
131
146
|
this.recordingsDir = recordingsDir;
|
|
132
147
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
148
|
+
this.recordingPromises = [];
|
|
133
149
|
this.proxy = httpProxy.createProxyServer({
|
|
134
150
|
secure: false,
|
|
135
151
|
changeOrigin: true,
|
|
@@ -155,7 +171,7 @@ var ProxyServer = class {
|
|
|
155
171
|
}
|
|
156
172
|
setupProxyEventHandlers() {
|
|
157
173
|
this.proxy.on("error", this.handleProxyError.bind(this));
|
|
158
|
-
this.proxy.on("proxyRes", this.
|
|
174
|
+
this.proxy.on("proxyRes", this.addCorsHeaders.bind(this));
|
|
159
175
|
}
|
|
160
176
|
handleProxyError(err, req, res) {
|
|
161
177
|
console.error("Proxy error:", err);
|
|
@@ -171,12 +187,6 @@ var ProxyServer = class {
|
|
|
171
187
|
}
|
|
172
188
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
173
189
|
}
|
|
174
|
-
handleProxyResponse(proxyRes, req) {
|
|
175
|
-
this.addCorsHeaders(proxyRes, req);
|
|
176
|
-
if (this.mode === Modes.record && this.recordingId) {
|
|
177
|
-
this.recordResponse(req, proxyRes);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
190
|
/**
|
|
181
191
|
* Get CORS headers for a given request
|
|
182
192
|
* @param req The incoming HTTP request
|
|
@@ -270,18 +280,20 @@ var ProxyServer = class {
|
|
|
270
280
|
}
|
|
271
281
|
return { mode, id, timeout };
|
|
272
282
|
}
|
|
283
|
+
async parseControlRequest(req) {
|
|
284
|
+
if (req.method === "GET") {
|
|
285
|
+
return this.parseGetParams(req);
|
|
286
|
+
}
|
|
287
|
+
if (req.method === "POST") {
|
|
288
|
+
const body = await readRequestBody(req);
|
|
289
|
+
console.log(`MODE CHANGE (${req.method})`, body);
|
|
290
|
+
return JSON.parse(body);
|
|
291
|
+
}
|
|
292
|
+
throw new Error("Unsupported control method");
|
|
293
|
+
}
|
|
273
294
|
async handleControlRequest(req, res) {
|
|
274
295
|
try {
|
|
275
|
-
|
|
276
|
-
if (req.method === "GET") {
|
|
277
|
-
data = this.parseGetParams(req);
|
|
278
|
-
} else if (req.method === "POST") {
|
|
279
|
-
const body = await readRequestBody(req);
|
|
280
|
-
console.log(`MODE CHANGE (${req.method})`, body);
|
|
281
|
-
data = JSON.parse(body);
|
|
282
|
-
} else {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
296
|
+
const data = await this.parseControlRequest(req);
|
|
285
297
|
const { mode, id, timeout: requestTimeout } = data;
|
|
286
298
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
287
299
|
this.clearModeTimeout();
|
|
@@ -314,7 +326,7 @@ var ProxyServer = class {
|
|
|
314
326
|
async switchMode(mode, id) {
|
|
315
327
|
console.log(`Switching to ${mode.toUpperCase()} mode`);
|
|
316
328
|
if (this.currentSession && this.mode === Modes.record) {
|
|
317
|
-
await this.saveCurrentSession(
|
|
329
|
+
await this.saveCurrentSession();
|
|
318
330
|
console.log("Session saved, continuing with mode switch");
|
|
319
331
|
}
|
|
320
332
|
switch (mode) {
|
|
@@ -354,6 +366,8 @@ var ProxyServer = class {
|
|
|
354
366
|
this.recordingId = id;
|
|
355
367
|
this.replayId = null;
|
|
356
368
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
369
|
+
this.recordingIdCounter = 0;
|
|
370
|
+
this.sequenceCounterByKey.clear();
|
|
357
371
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
358
372
|
}
|
|
359
373
|
async switchToReplayMode(id) {
|
|
@@ -373,146 +387,39 @@ var ProxyServer = class {
|
|
|
373
387
|
setupModeTimeout(timeout) {
|
|
374
388
|
this.modeTimeout = setTimeout(async () => {
|
|
375
389
|
console.log("Timeout reached, switching back to transparent mode");
|
|
376
|
-
await this.saveCurrentSession(
|
|
390
|
+
await this.saveCurrentSession();
|
|
377
391
|
this.switchToTransparentMode();
|
|
378
392
|
this.modeTimeout = null;
|
|
379
393
|
}, timeout);
|
|
380
394
|
}
|
|
381
|
-
async
|
|
382
|
-
if (
|
|
395
|
+
async flushPendingRecordings() {
|
|
396
|
+
if (this.recordingPromises.length === 0) {
|
|
383
397
|
return;
|
|
384
398
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
(r) => r.response
|
|
392
|
-
);
|
|
399
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
400
|
+
if (this.currentSession) {
|
|
401
|
+
for (const result of results) {
|
|
402
|
+
if (result.status === "fulfilled" && result.value) {
|
|
403
|
+
this.currentSession.recordings.push(result.value);
|
|
404
|
+
}
|
|
393
405
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
397
|
-
);
|
|
398
|
-
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
399
|
-
}
|
|
400
|
-
saveRequestRecordSync(req, body) {
|
|
401
|
-
if (!this.currentSession) {
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const key = getReqID(req);
|
|
405
|
-
const recordingId = this.recordingIdCounter++;
|
|
406
|
-
req.__recordingId = recordingId;
|
|
407
|
-
const record = {
|
|
408
|
-
request: {
|
|
409
|
-
method: req.method,
|
|
410
|
-
url: req.url,
|
|
411
|
-
headers: req.headers,
|
|
412
|
-
body: body || null
|
|
413
|
-
},
|
|
414
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
415
|
-
key,
|
|
416
|
-
recordingId
|
|
417
|
-
};
|
|
418
|
-
this.currentSession.recordings.push(record);
|
|
419
|
-
console.log(
|
|
420
|
-
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
421
|
-
`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})`
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
updateRequestBodySync(req, body) {
|
|
425
|
-
if (!this.currentSession) {
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
const recordingId = req.__recordingId;
|
|
429
|
-
if (recordingId === void 0) {
|
|
430
|
-
console.error(
|
|
431
|
-
`updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
|
|
432
|
-
);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const record = this.currentSession.recordings.find(
|
|
436
|
-
(r) => r.recordingId === recordingId
|
|
437
|
-
);
|
|
438
|
-
if (!record) {
|
|
439
|
-
console.error(
|
|
440
|
-
`updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
406
|
+
console.log(
|
|
407
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
441
408
|
);
|
|
442
|
-
return;
|
|
443
409
|
}
|
|
444
|
-
|
|
445
|
-
console.log(
|
|
446
|
-
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
|
|
447
|
-
);
|
|
410
|
+
this.recordingPromises = [];
|
|
448
411
|
}
|
|
449
|
-
async
|
|
412
|
+
async saveCurrentSession() {
|
|
450
413
|
if (!this.currentSession) {
|
|
451
414
|
return;
|
|
452
415
|
}
|
|
453
|
-
|
|
454
|
-
if (recordingId === void 0) {
|
|
455
|
-
console.error(
|
|
456
|
-
`recordResponse: No recording ID found on request ${req.method} ${req.url}`
|
|
457
|
-
);
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
const record = this.currentSession.recordings.find(
|
|
461
|
-
(r) => r.recordingId === recordingId
|
|
462
|
-
);
|
|
463
|
-
if (!record) {
|
|
464
|
-
console.error(
|
|
465
|
-
`recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
466
|
-
);
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
const chunks = [];
|
|
470
|
-
proxyRes.on("data", (chunk) => {
|
|
471
|
-
chunks.push(chunk);
|
|
472
|
-
});
|
|
473
|
-
proxyRes.on("end", async () => {
|
|
474
|
-
const body = Buffer.concat(chunks).toString("utf8");
|
|
475
|
-
record.response = {
|
|
476
|
-
statusCode: proxyRes.statusCode,
|
|
477
|
-
headers: proxyRes.headers,
|
|
478
|
-
body: body || null
|
|
479
|
-
};
|
|
480
|
-
console.log(
|
|
481
|
-
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
482
|
-
);
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
async recordResponseData(req, proxyRes, body) {
|
|
486
|
-
if (!this.currentSession) {
|
|
487
|
-
return false;
|
|
488
|
-
}
|
|
489
|
-
const recordingId = req.__recordingId;
|
|
490
|
-
if (recordingId === void 0) {
|
|
491
|
-
console.error(
|
|
492
|
-
`recordResponseData: No recording ID found on request ${req.method} ${req.url}`
|
|
493
|
-
);
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
const record = this.currentSession.recordings.find(
|
|
497
|
-
(r) => r.recordingId === recordingId
|
|
498
|
-
);
|
|
499
|
-
if (!record) {
|
|
500
|
-
console.error(
|
|
501
|
-
`recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
502
|
-
);
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
record.response = {
|
|
506
|
-
statusCode: proxyRes.statusCode,
|
|
507
|
-
headers: proxyRes.headers,
|
|
508
|
-
body: body || null
|
|
509
|
-
};
|
|
416
|
+
await this.flushPendingRecordings();
|
|
510
417
|
console.log(
|
|
511
|
-
`
|
|
418
|
+
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
512
419
|
);
|
|
513
|
-
|
|
420
|
+
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
514
421
|
}
|
|
515
|
-
|
|
422
|
+
getRecordingIdOrError(req, res) {
|
|
516
423
|
const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
|
|
517
424
|
if (!recordingId) {
|
|
518
425
|
const corsHeaders = this.getCorsHeaders(req);
|
|
@@ -521,21 +428,50 @@ var ProxyServer = class {
|
|
|
521
428
|
...corsHeaders
|
|
522
429
|
});
|
|
523
430
|
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
524
|
-
return;
|
|
431
|
+
return null;
|
|
525
432
|
}
|
|
433
|
+
return recordingId;
|
|
434
|
+
}
|
|
435
|
+
async ensureSessionLoaded(recordingId, filePath) {
|
|
436
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
437
|
+
if (!sessionState.loadedSession) {
|
|
438
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
439
|
+
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
440
|
+
}
|
|
441
|
+
return sessionState;
|
|
442
|
+
}
|
|
443
|
+
getServedTracker(sessionState, key) {
|
|
444
|
+
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
445
|
+
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
446
|
+
}
|
|
447
|
+
return sessionState.servedRecordingIdsByKey.get(key);
|
|
448
|
+
}
|
|
449
|
+
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
450
|
+
for (const rec of recordsWithKey) {
|
|
451
|
+
if (!servedForThisKey.has(rec.recordingId)) {
|
|
452
|
+
return rec;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (recordsWithKey.length > 0) {
|
|
456
|
+
console.log(
|
|
457
|
+
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
458
|
+
);
|
|
459
|
+
return recordsWithKey[recordsWithKey.length - 1];
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
async handleReplayRequest(req, res) {
|
|
464
|
+
const recordingId = this.getRecordingIdOrError(req, res);
|
|
465
|
+
if (!recordingId) return;
|
|
526
466
|
const key = getReqID(req);
|
|
527
467
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
528
468
|
try {
|
|
529
|
-
const sessionState = this.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
469
|
+
const sessionState = await this.ensureSessionLoaded(
|
|
470
|
+
recordingId,
|
|
471
|
+
filePath
|
|
472
|
+
);
|
|
534
473
|
const session = sessionState.loadedSession;
|
|
535
|
-
|
|
536
|
-
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
537
|
-
}
|
|
538
|
-
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
474
|
+
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
539
475
|
const host = req.headers.host || "unknown";
|
|
540
476
|
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
541
477
|
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
@@ -564,30 +500,23 @@ var ProxyServer = class {
|
|
|
564
500
|
}
|
|
565
501
|
const requestCount = servedForThisKey.size + 1;
|
|
566
502
|
console.log(
|
|
567
|
-
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
503
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (key: ${key}, session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
568
504
|
);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
505
|
+
const record = this.selectReplayRecord(
|
|
506
|
+
recordsWithKey,
|
|
507
|
+
servedForThisKey,
|
|
508
|
+
key,
|
|
509
|
+
recordingId
|
|
510
|
+
);
|
|
511
|
+
if (!record || !record.response) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
579
514
|
);
|
|
580
|
-
record = recordsWithKey[recordsWithKey.length - 1];
|
|
581
515
|
}
|
|
582
516
|
servedForThisKey.add(record.recordingId);
|
|
583
517
|
console.log(
|
|
584
518
|
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
585
519
|
);
|
|
586
|
-
if (!record.response) {
|
|
587
|
-
throw new Error(
|
|
588
|
-
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
520
|
const { statusCode, headers, body } = record.response;
|
|
592
521
|
const responseHeaders = {
|
|
593
522
|
...headers,
|
|
@@ -642,82 +571,124 @@ var ProxyServer = class {
|
|
|
642
571
|
const target = this.getTarget();
|
|
643
572
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
644
573
|
if (this.mode === Modes.record) {
|
|
645
|
-
this.
|
|
646
|
-
await this.bufferAndProxyRequest(req, res, target);
|
|
574
|
+
await this.recordAndProxyRequest(req, res, target);
|
|
647
575
|
} else {
|
|
648
576
|
this.proxy.web(req, res, { target });
|
|
649
577
|
}
|
|
650
578
|
}
|
|
651
|
-
//
|
|
652
|
-
async
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
chunks.push(chunk);
|
|
656
|
-
});
|
|
657
|
-
try {
|
|
658
|
-
await new Promise((resolve, reject) => {
|
|
659
|
-
req.on("end", () => resolve());
|
|
660
|
-
req.on("error", (err) => reject(err));
|
|
661
|
-
setTimeout(
|
|
662
|
-
() => reject(new Error("Request buffering timeout")),
|
|
663
|
-
3e4
|
|
664
|
-
);
|
|
665
|
-
});
|
|
666
|
-
} catch (error) {
|
|
667
|
-
console.error("Error buffering request:", error);
|
|
579
|
+
// Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
|
|
580
|
+
async recordAndProxyRequest(req, res, target) {
|
|
581
|
+
if (!this.currentSession) {
|
|
582
|
+
return;
|
|
668
583
|
}
|
|
669
|
-
const
|
|
670
|
-
this.
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const recorded = await this.recordResponseData(
|
|
692
|
-
req,
|
|
693
|
-
proxyRes,
|
|
694
|
-
responseBody.toString("utf8")
|
|
695
|
-
);
|
|
696
|
-
const responseHeaders = {
|
|
697
|
-
...proxyRes.headers,
|
|
698
|
-
...this.getCorsHeaders(req)
|
|
699
|
-
};
|
|
700
|
-
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
701
|
-
res.end(responseBody);
|
|
702
|
-
if (recorded) {
|
|
703
|
-
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
584
|
+
const key = getReqID(req);
|
|
585
|
+
const recordingId = this.recordingIdCounter++;
|
|
586
|
+
const sequence = this.sequenceCounterByKey.get(key) || 0;
|
|
587
|
+
this.sequenceCounterByKey.set(key, sequence + 1);
|
|
588
|
+
const recordingPromise = new Promise((resolve) => {
|
|
589
|
+
(async () => {
|
|
590
|
+
try {
|
|
591
|
+
const chunks = [];
|
|
592
|
+
req.on("data", (chunk) => {
|
|
593
|
+
chunks.push(chunk);
|
|
594
|
+
});
|
|
595
|
+
try {
|
|
596
|
+
await new Promise((resolveBuffer, rejectBuffer) => {
|
|
597
|
+
req.on("end", () => resolveBuffer());
|
|
598
|
+
req.on("error", (err) => rejectBuffer(err));
|
|
599
|
+
setTimeout(
|
|
600
|
+
() => rejectBuffer(new Error("Request buffering timeout")),
|
|
601
|
+
3e4
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error("Error buffering request:", error);
|
|
704
606
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
607
|
+
const requestBody = Buffer.concat(chunks).toString("utf8");
|
|
608
|
+
const targetUrl = new URL(target);
|
|
609
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
610
|
+
const requestModule = isHttps ? https : http;
|
|
611
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
612
|
+
const proxyReq = requestModule.request(
|
|
613
|
+
{
|
|
614
|
+
hostname: targetUrl.hostname,
|
|
615
|
+
port: targetUrl.port || defaultPort,
|
|
616
|
+
path: req.url,
|
|
617
|
+
method: req.method,
|
|
618
|
+
headers: req.headers
|
|
619
|
+
},
|
|
620
|
+
(proxyRes) => {
|
|
621
|
+
this.addCorsHeaders(proxyRes, req);
|
|
622
|
+
const responseChunks = [];
|
|
623
|
+
proxyRes.on("data", (chunk) => {
|
|
624
|
+
responseChunks.push(chunk);
|
|
625
|
+
});
|
|
626
|
+
proxyRes.on("end", async () => {
|
|
627
|
+
try {
|
|
628
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
629
|
+
const responseBodyStr = responseBody.toString("utf8");
|
|
630
|
+
const recording = {
|
|
631
|
+
request: {
|
|
632
|
+
method: req.method,
|
|
633
|
+
url: req.url,
|
|
634
|
+
headers: req.headers,
|
|
635
|
+
body: requestBody || null
|
|
636
|
+
},
|
|
637
|
+
response: {
|
|
638
|
+
statusCode: proxyRes.statusCode,
|
|
639
|
+
headers: proxyRes.headers,
|
|
640
|
+
body: responseBodyStr || null
|
|
641
|
+
},
|
|
642
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
643
|
+
key,
|
|
644
|
+
recordingId,
|
|
645
|
+
sequence
|
|
646
|
+
};
|
|
647
|
+
const responseHeaders = {
|
|
648
|
+
...proxyRes.headers,
|
|
649
|
+
...this.getCorsHeaders(req)
|
|
650
|
+
};
|
|
651
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
652
|
+
res.end(responseBody);
|
|
653
|
+
console.log(
|
|
654
|
+
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId}, sequence: ${sequence})`
|
|
655
|
+
);
|
|
656
|
+
resolve(recording);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error("Error completing recording:", error);
|
|
659
|
+
resolve(null);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
proxyRes.on("error", (err) => {
|
|
663
|
+
console.error("Proxy response error:", err);
|
|
664
|
+
if (!res.headersSent) {
|
|
665
|
+
this.handleProxyError(err, req, res);
|
|
666
|
+
}
|
|
667
|
+
resolve(null);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
proxyReq.on("error", (err) => {
|
|
709
672
|
this.handleProxyError(err, req, res);
|
|
673
|
+
resolve(null);
|
|
674
|
+
});
|
|
675
|
+
if (chunks.length > 0) {
|
|
676
|
+
proxyReq.write(Buffer.concat(chunks));
|
|
710
677
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
678
|
+
proxyReq.end();
|
|
679
|
+
} catch (error) {
|
|
680
|
+
console.error("Error in recordAndProxyRequest:", error);
|
|
681
|
+
try {
|
|
682
|
+
this.handleProxyError(error, req, res);
|
|
683
|
+
} catch (error_) {
|
|
684
|
+
console.error("Failed to handle proxy error:", error_);
|
|
685
|
+
}
|
|
686
|
+
resolve(null);
|
|
687
|
+
}
|
|
688
|
+
})();
|
|
716
689
|
});
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
proxyReq.end();
|
|
690
|
+
this.recordingPromises.push(recordingPromise);
|
|
691
|
+
await recordingPromise;
|
|
721
692
|
}
|
|
722
693
|
handleUpgrade(req, socket, head) {
|
|
723
694
|
if (this.mode === Modes.replay) {
|
|
@@ -979,7 +950,7 @@ var playwrightProxy = {
|
|
|
979
950
|
try {
|
|
980
951
|
await setProxyMode(Modes.replay, sessionId);
|
|
981
952
|
console.log(
|
|
982
|
-
`[Cleanup] Switched to
|
|
953
|
+
`[Cleanup] Switched to replay mode for session: ${sessionId}`
|
|
983
954
|
);
|
|
984
955
|
} catch (error) {
|
|
985
956
|
console.error("[Cleanup] Error during page close cleanup:", error);
|
|
@@ -106,7 +106,7 @@ var playwrightProxy = {
|
|
|
106
106
|
try {
|
|
107
107
|
await setProxyMode(Modes.replay, sessionId);
|
|
108
108
|
console.log(
|
|
109
|
-
`[Cleanup] Switched to
|
|
109
|
+
`[Cleanup] Switched to replay mode for session: ${sessionId}`
|
|
110
110
|
);
|
|
111
111
|
} catch (error) {
|
|
112
112
|
console.error("[Cleanup] Error during page close cleanup:", error);
|
|
@@ -104,7 +104,7 @@ var playwrightProxy = {
|
|
|
104
104
|
try {
|
|
105
105
|
await setProxyMode(Modes.replay, sessionId);
|
|
106
106
|
console.log(
|
|
107
|
-
`[Cleanup] Switched to
|
|
107
|
+
`[Cleanup] Switched to replay mode for session: ${sessionId}`
|
|
108
108
|
);
|
|
109
109
|
} catch (error) {
|
|
110
110
|
console.error("[Cleanup] Error during page close cleanup:", error);
|