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/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Add to `package.json`:
|
|
|
59
59
|
```json
|
|
60
60
|
{
|
|
61
61
|
"scripts": {
|
|
62
|
-
"proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --
|
|
62
|
+
"proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --dir ./e2e/recordings"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
```
|
|
@@ -73,7 +73,7 @@ npm install --save-dev concurrently
|
|
|
73
73
|
```json
|
|
74
74
|
{
|
|
75
75
|
"scripts": {
|
|
76
|
-
"proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --
|
|
76
|
+
"proxy": "test-proxy-recorder http://localhost:8000 --port 8100 --dir ./e2e/recordings",
|
|
77
77
|
"dev:proxy": "concurrently -n \"proxy,app\" -c \"blue,green\" \"npm run proxy\" \"INTERNAL_API_URL=http://localhost:8100 npm run dev\""
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -165,7 +165,7 @@ test-proxy-recorder <target-url> [options]
|
|
|
165
165
|
|
|
166
166
|
- `<target-url>` - Backend API URL (positional argument, required)
|
|
167
167
|
- `--port, -p <number>` - Port to listen on (default: 8080)
|
|
168
|
-
- `--
|
|
168
|
+
- `--dir, -d <path>` - Directory to store recordings (default: ./recordings)
|
|
169
169
|
- `--help, -h` - Show help
|
|
170
170
|
|
|
171
171
|
### Examples
|
|
@@ -175,7 +175,7 @@ test-proxy-recorder <target-url> [options]
|
|
|
175
175
|
test-proxy-recorder http://localhost:8000
|
|
176
176
|
|
|
177
177
|
# Custom port and recordings directory
|
|
178
|
-
test-proxy-recorder http://localhost:8000 --port 8100 --
|
|
178
|
+
test-proxy-recorder http://localhost:8000 --port 8100 --dir ./mocks
|
|
179
179
|
|
|
180
180
|
# Multiple targets (experimental)
|
|
181
181
|
test-proxy-recorder http://localhost:8000 http://localhost:9000 --port 8100
|
package/dist/index.cjs
CHANGED
|
@@ -60,13 +60,23 @@ async function loadRecordingSession(filePath) {
|
|
|
60
60
|
return JSON.parse(fileContent);
|
|
61
61
|
}
|
|
62
62
|
function processRecordings(recordings) {
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const recordingsByKey = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const recording of recordings) {
|
|
65
65
|
const key = recording.key;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
if (!recordingsByKey.has(key)) {
|
|
67
|
+
recordingsByKey.set(key, []);
|
|
68
|
+
}
|
|
69
|
+
recordingsByKey.get(key).push(recording);
|
|
70
|
+
}
|
|
71
|
+
const processedRecordings = [];
|
|
72
|
+
for (const [_key, keyRecordings] of recordingsByKey) {
|
|
73
|
+
keyRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
74
|
+
keyRecordings.forEach((recording, index) => {
|
|
75
|
+
processedRecordings.push({ ...recording, sequence: index });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
79
|
+
return processedRecordings;
|
|
70
80
|
}
|
|
71
81
|
async function saveRecordingSession(recordingsDir, session) {
|
|
72
82
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
@@ -129,19 +139,25 @@ var ProxyServer = class {
|
|
|
129
139
|
recordingsDir;
|
|
130
140
|
recordingIdCounter;
|
|
131
141
|
// Unique ID for each recording entry
|
|
142
|
+
sequenceCounterByKey;
|
|
143
|
+
// Sequence counter per key (endpoint)
|
|
132
144
|
replaySessions;
|
|
133
145
|
// Track multiple concurrent replay sessions by recording ID
|
|
146
|
+
recordingPromises;
|
|
147
|
+
// Stack of promises that resolve to completed recordings
|
|
134
148
|
constructor(targets, recordingsDir) {
|
|
135
149
|
this.targets = targets;
|
|
136
150
|
this.currentTargetIndex = 0;
|
|
137
151
|
this.mode = Modes.transparent;
|
|
138
152
|
this.recordingId = null;
|
|
139
153
|
this.recordingIdCounter = 0;
|
|
154
|
+
this.sequenceCounterByKey = /* @__PURE__ */ new Map();
|
|
140
155
|
this.replayId = null;
|
|
141
156
|
this.modeTimeout = null;
|
|
142
157
|
this.currentSession = null;
|
|
143
158
|
this.recordingsDir = recordingsDir;
|
|
144
159
|
this.replaySessions = /* @__PURE__ */ new Map();
|
|
160
|
+
this.recordingPromises = [];
|
|
145
161
|
this.proxy = httpProxy__default.default.createProxyServer({
|
|
146
162
|
secure: false,
|
|
147
163
|
changeOrigin: true,
|
|
@@ -167,7 +183,7 @@ var ProxyServer = class {
|
|
|
167
183
|
}
|
|
168
184
|
setupProxyEventHandlers() {
|
|
169
185
|
this.proxy.on("error", this.handleProxyError.bind(this));
|
|
170
|
-
this.proxy.on("proxyRes", this.
|
|
186
|
+
this.proxy.on("proxyRes", this.addCorsHeaders.bind(this));
|
|
171
187
|
}
|
|
172
188
|
handleProxyError(err, req, res) {
|
|
173
189
|
console.error("Proxy error:", err);
|
|
@@ -183,12 +199,6 @@ var ProxyServer = class {
|
|
|
183
199
|
}
|
|
184
200
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
185
201
|
}
|
|
186
|
-
handleProxyResponse(proxyRes, req) {
|
|
187
|
-
this.addCorsHeaders(proxyRes, req);
|
|
188
|
-
if (this.mode === Modes.record && this.recordingId) {
|
|
189
|
-
this.recordResponse(req, proxyRes);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
202
|
/**
|
|
193
203
|
* Get CORS headers for a given request
|
|
194
204
|
* @param req The incoming HTTP request
|
|
@@ -282,18 +292,20 @@ var ProxyServer = class {
|
|
|
282
292
|
}
|
|
283
293
|
return { mode, id, timeout };
|
|
284
294
|
}
|
|
295
|
+
async parseControlRequest(req) {
|
|
296
|
+
if (req.method === "GET") {
|
|
297
|
+
return this.parseGetParams(req);
|
|
298
|
+
}
|
|
299
|
+
if (req.method === "POST") {
|
|
300
|
+
const body = await readRequestBody(req);
|
|
301
|
+
console.log(`MODE CHANGE (${req.method})`, body);
|
|
302
|
+
return JSON.parse(body);
|
|
303
|
+
}
|
|
304
|
+
throw new Error("Unsupported control method");
|
|
305
|
+
}
|
|
285
306
|
async handleControlRequest(req, res) {
|
|
286
307
|
try {
|
|
287
|
-
|
|
288
|
-
if (req.method === "GET") {
|
|
289
|
-
data = this.parseGetParams(req);
|
|
290
|
-
} else if (req.method === "POST") {
|
|
291
|
-
const body = await readRequestBody(req);
|
|
292
|
-
console.log(`MODE CHANGE (${req.method})`, body);
|
|
293
|
-
data = JSON.parse(body);
|
|
294
|
-
} else {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
308
|
+
const data = await this.parseControlRequest(req);
|
|
297
309
|
const { mode, id, timeout: requestTimeout } = data;
|
|
298
310
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
299
311
|
this.clearModeTimeout();
|
|
@@ -326,7 +338,7 @@ var ProxyServer = class {
|
|
|
326
338
|
async switchMode(mode, id) {
|
|
327
339
|
console.log(`Switching to ${mode.toUpperCase()} mode`);
|
|
328
340
|
if (this.currentSession && this.mode === Modes.record) {
|
|
329
|
-
await this.saveCurrentSession(
|
|
341
|
+
await this.saveCurrentSession();
|
|
330
342
|
console.log("Session saved, continuing with mode switch");
|
|
331
343
|
}
|
|
332
344
|
switch (mode) {
|
|
@@ -366,6 +378,8 @@ var ProxyServer = class {
|
|
|
366
378
|
this.recordingId = id;
|
|
367
379
|
this.replayId = null;
|
|
368
380
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
381
|
+
this.recordingIdCounter = 0;
|
|
382
|
+
this.sequenceCounterByKey.clear();
|
|
369
383
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
370
384
|
}
|
|
371
385
|
async switchToReplayMode(id) {
|
|
@@ -385,146 +399,39 @@ var ProxyServer = class {
|
|
|
385
399
|
setupModeTimeout(timeout) {
|
|
386
400
|
this.modeTimeout = setTimeout(async () => {
|
|
387
401
|
console.log("Timeout reached, switching back to transparent mode");
|
|
388
|
-
await this.saveCurrentSession(
|
|
402
|
+
await this.saveCurrentSession();
|
|
389
403
|
this.switchToTransparentMode();
|
|
390
404
|
this.modeTimeout = null;
|
|
391
405
|
}, timeout);
|
|
392
406
|
}
|
|
393
|
-
async
|
|
394
|
-
if (
|
|
407
|
+
async flushPendingRecordings() {
|
|
408
|
+
if (this.recordingPromises.length === 0) {
|
|
395
409
|
return;
|
|
396
410
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
(r) => r.response
|
|
404
|
-
);
|
|
411
|
+
const results = await Promise.allSettled(this.recordingPromises);
|
|
412
|
+
if (this.currentSession) {
|
|
413
|
+
for (const result of results) {
|
|
414
|
+
if (result.status === "fulfilled" && result.value) {
|
|
415
|
+
this.currentSession.recordings.push(result.value);
|
|
416
|
+
}
|
|
405
417
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
409
|
-
);
|
|
410
|
-
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
411
|
-
}
|
|
412
|
-
saveRequestRecordSync(req, body) {
|
|
413
|
-
if (!this.currentSession) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
const key = getReqID(req);
|
|
417
|
-
const recordingId = this.recordingIdCounter++;
|
|
418
|
-
req.__recordingId = recordingId;
|
|
419
|
-
const record = {
|
|
420
|
-
request: {
|
|
421
|
-
method: req.method,
|
|
422
|
-
url: req.url,
|
|
423
|
-
headers: req.headers,
|
|
424
|
-
body: body || null
|
|
425
|
-
},
|
|
426
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
427
|
-
key,
|
|
428
|
-
recordingId
|
|
429
|
-
};
|
|
430
|
-
this.currentSession.recordings.push(record);
|
|
431
|
-
console.log(
|
|
432
|
-
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
433
|
-
`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})`
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
updateRequestBodySync(req, body) {
|
|
437
|
-
if (!this.currentSession) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const recordingId = req.__recordingId;
|
|
441
|
-
if (recordingId === void 0) {
|
|
442
|
-
console.error(
|
|
443
|
-
`updateRequestBodySync: No recording ID found on request ${req.method} ${req.url}`
|
|
444
|
-
);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
const record = this.currentSession.recordings.find(
|
|
448
|
-
(r) => r.recordingId === recordingId
|
|
449
|
-
);
|
|
450
|
-
if (!record) {
|
|
451
|
-
console.error(
|
|
452
|
-
`updateRequestBodySync: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
418
|
+
console.log(
|
|
419
|
+
`Flushed ${results.length} recordings to session (total: ${this.currentSession.recordings.length})`
|
|
453
420
|
);
|
|
454
|
-
return;
|
|
455
421
|
}
|
|
456
|
-
|
|
457
|
-
console.log(
|
|
458
|
-
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars, recordingId: ${recordingId})`
|
|
459
|
-
);
|
|
422
|
+
this.recordingPromises = [];
|
|
460
423
|
}
|
|
461
|
-
async
|
|
424
|
+
async saveCurrentSession() {
|
|
462
425
|
if (!this.currentSession) {
|
|
463
426
|
return;
|
|
464
427
|
}
|
|
465
|
-
|
|
466
|
-
if (recordingId === void 0) {
|
|
467
|
-
console.error(
|
|
468
|
-
`recordResponse: No recording ID found on request ${req.method} ${req.url}`
|
|
469
|
-
);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const record = this.currentSession.recordings.find(
|
|
473
|
-
(r) => r.recordingId === recordingId
|
|
474
|
-
);
|
|
475
|
-
if (!record) {
|
|
476
|
-
console.error(
|
|
477
|
-
`recordResponse: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
478
|
-
);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
const chunks = [];
|
|
482
|
-
proxyRes.on("data", (chunk) => {
|
|
483
|
-
chunks.push(chunk);
|
|
484
|
-
});
|
|
485
|
-
proxyRes.on("end", async () => {
|
|
486
|
-
const body = Buffer.concat(chunks).toString("utf8");
|
|
487
|
-
record.response = {
|
|
488
|
-
statusCode: proxyRes.statusCode,
|
|
489
|
-
headers: proxyRes.headers,
|
|
490
|
-
body: body || null
|
|
491
|
-
};
|
|
492
|
-
console.log(
|
|
493
|
-
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
494
|
-
);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
async recordResponseData(req, proxyRes, body) {
|
|
498
|
-
if (!this.currentSession) {
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
const recordingId = req.__recordingId;
|
|
502
|
-
if (recordingId === void 0) {
|
|
503
|
-
console.error(
|
|
504
|
-
`recordResponseData: No recording ID found on request ${req.method} ${req.url}`
|
|
505
|
-
);
|
|
506
|
-
return false;
|
|
507
|
-
}
|
|
508
|
-
const record = this.currentSession.recordings.find(
|
|
509
|
-
(r) => r.recordingId === recordingId
|
|
510
|
-
);
|
|
511
|
-
if (!record) {
|
|
512
|
-
console.error(
|
|
513
|
-
`recordResponseData: Could not find recording with ID ${recordingId} for ${req.method} ${req.url}`
|
|
514
|
-
);
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
record.response = {
|
|
518
|
-
statusCode: proxyRes.statusCode,
|
|
519
|
-
headers: proxyRes.headers,
|
|
520
|
-
body: body || null
|
|
521
|
-
};
|
|
428
|
+
await this.flushPendingRecordings();
|
|
522
429
|
console.log(
|
|
523
|
-
`
|
|
430
|
+
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
524
431
|
);
|
|
525
|
-
|
|
432
|
+
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
526
433
|
}
|
|
527
|
-
|
|
434
|
+
getRecordingIdOrError(req, res) {
|
|
528
435
|
const recordingId = this.getRecordingIdFromRequest(req) || this.replayId;
|
|
529
436
|
if (!recordingId) {
|
|
530
437
|
const corsHeaders = this.getCorsHeaders(req);
|
|
@@ -533,21 +440,50 @@ var ProxyServer = class {
|
|
|
533
440
|
...corsHeaders
|
|
534
441
|
});
|
|
535
442
|
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
536
|
-
return;
|
|
443
|
+
return null;
|
|
537
444
|
}
|
|
445
|
+
return recordingId;
|
|
446
|
+
}
|
|
447
|
+
async ensureSessionLoaded(recordingId, filePath) {
|
|
448
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
449
|
+
if (!sessionState.loadedSession) {
|
|
450
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
451
|
+
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
452
|
+
}
|
|
453
|
+
return sessionState;
|
|
454
|
+
}
|
|
455
|
+
getServedTracker(sessionState, key) {
|
|
456
|
+
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
457
|
+
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
458
|
+
}
|
|
459
|
+
return sessionState.servedRecordingIdsByKey.get(key);
|
|
460
|
+
}
|
|
461
|
+
selectReplayRecord(recordsWithKey, servedForThisKey, key, recordingId) {
|
|
462
|
+
for (const rec of recordsWithKey) {
|
|
463
|
+
if (!servedForThisKey.has(rec.recordingId)) {
|
|
464
|
+
return rec;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (recordsWithKey.length > 0) {
|
|
468
|
+
console.log(
|
|
469
|
+
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
470
|
+
);
|
|
471
|
+
return recordsWithKey[recordsWithKey.length - 1];
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
async handleReplayRequest(req, res) {
|
|
476
|
+
const recordingId = this.getRecordingIdOrError(req, res);
|
|
477
|
+
if (!recordingId) return;
|
|
538
478
|
const key = getReqID(req);
|
|
539
479
|
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
540
480
|
try {
|
|
541
|
-
const sessionState = this.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
481
|
+
const sessionState = await this.ensureSessionLoaded(
|
|
482
|
+
recordingId,
|
|
483
|
+
filePath
|
|
484
|
+
);
|
|
546
485
|
const session = sessionState.loadedSession;
|
|
547
|
-
|
|
548
|
-
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
549
|
-
}
|
|
550
|
-
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
486
|
+
const servedForThisKey = this.getServedTracker(sessionState, key);
|
|
551
487
|
const host = req.headers.host || "unknown";
|
|
552
488
|
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => {
|
|
553
489
|
const aSeq = a.sequence !== void 0 ? a.sequence : a.recordingId;
|
|
@@ -576,30 +512,23 @@ var ProxyServer = class {
|
|
|
576
512
|
}
|
|
577
513
|
const requestCount = servedForThisKey.size + 1;
|
|
578
514
|
console.log(
|
|
579
|
-
`[replay request #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
515
|
+
`[replay request #${requestCount}] ${req.method} ${req.url} (key: ${key}, session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
580
516
|
);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
517
|
+
const record = this.selectReplayRecord(
|
|
518
|
+
recordsWithKey,
|
|
519
|
+
servedForThisKey,
|
|
520
|
+
key,
|
|
521
|
+
recordingId
|
|
522
|
+
);
|
|
523
|
+
if (!record || !record.response) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
591
526
|
);
|
|
592
|
-
record = recordsWithKey[recordsWithKey.length - 1];
|
|
593
527
|
}
|
|
594
528
|
servedForThisKey.add(record.recordingId);
|
|
595
529
|
console.log(
|
|
596
530
|
`[replay serving] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
597
531
|
);
|
|
598
|
-
if (!record.response) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
532
|
const { statusCode, headers, body } = record.response;
|
|
604
533
|
const responseHeaders = {
|
|
605
534
|
...headers,
|
|
@@ -654,82 +583,124 @@ var ProxyServer = class {
|
|
|
654
583
|
const target = this.getTarget();
|
|
655
584
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
656
585
|
if (this.mode === Modes.record) {
|
|
657
|
-
this.
|
|
658
|
-
await this.bufferAndProxyRequest(req, res, target);
|
|
586
|
+
await this.recordAndProxyRequest(req, res, target);
|
|
659
587
|
} else {
|
|
660
588
|
this.proxy.web(req, res, { target });
|
|
661
589
|
}
|
|
662
590
|
}
|
|
663
|
-
//
|
|
664
|
-
async
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
chunks.push(chunk);
|
|
668
|
-
});
|
|
669
|
-
try {
|
|
670
|
-
await new Promise((resolve, reject) => {
|
|
671
|
-
req.on("end", () => resolve());
|
|
672
|
-
req.on("error", (err) => reject(err));
|
|
673
|
-
setTimeout(
|
|
674
|
-
() => reject(new Error("Request buffering timeout")),
|
|
675
|
-
3e4
|
|
676
|
-
);
|
|
677
|
-
});
|
|
678
|
-
} catch (error) {
|
|
679
|
-
console.error("Error buffering request:", error);
|
|
591
|
+
// Note: streaming requests are buffered before proxying; streaming passthrough is not yet implemented
|
|
592
|
+
async recordAndProxyRequest(req, res, target) {
|
|
593
|
+
if (!this.currentSession) {
|
|
594
|
+
return;
|
|
680
595
|
}
|
|
681
|
-
const
|
|
682
|
-
this.
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const recorded = await this.recordResponseData(
|
|
704
|
-
req,
|
|
705
|
-
proxyRes,
|
|
706
|
-
responseBody.toString("utf8")
|
|
707
|
-
);
|
|
708
|
-
const responseHeaders = {
|
|
709
|
-
...proxyRes.headers,
|
|
710
|
-
...this.getCorsHeaders(req)
|
|
711
|
-
};
|
|
712
|
-
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
713
|
-
res.end(responseBody);
|
|
714
|
-
if (recorded) {
|
|
715
|
-
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
596
|
+
const key = getReqID(req);
|
|
597
|
+
const recordingId = this.recordingIdCounter++;
|
|
598
|
+
const sequence = this.sequenceCounterByKey.get(key) || 0;
|
|
599
|
+
this.sequenceCounterByKey.set(key, sequence + 1);
|
|
600
|
+
const recordingPromise = new Promise((resolve) => {
|
|
601
|
+
(async () => {
|
|
602
|
+
try {
|
|
603
|
+
const chunks = [];
|
|
604
|
+
req.on("data", (chunk) => {
|
|
605
|
+
chunks.push(chunk);
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
await new Promise((resolveBuffer, rejectBuffer) => {
|
|
609
|
+
req.on("end", () => resolveBuffer());
|
|
610
|
+
req.on("error", (err) => rejectBuffer(err));
|
|
611
|
+
setTimeout(
|
|
612
|
+
() => rejectBuffer(new Error("Request buffering timeout")),
|
|
613
|
+
3e4
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error("Error buffering request:", error);
|
|
716
618
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
619
|
+
const requestBody = Buffer.concat(chunks).toString("utf8");
|
|
620
|
+
const targetUrl = new URL(target);
|
|
621
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
622
|
+
const requestModule = isHttps ? https__default.default : http__default.default;
|
|
623
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
624
|
+
const proxyReq = requestModule.request(
|
|
625
|
+
{
|
|
626
|
+
hostname: targetUrl.hostname,
|
|
627
|
+
port: targetUrl.port || defaultPort,
|
|
628
|
+
path: req.url,
|
|
629
|
+
method: req.method,
|
|
630
|
+
headers: req.headers
|
|
631
|
+
},
|
|
632
|
+
(proxyRes) => {
|
|
633
|
+
this.addCorsHeaders(proxyRes, req);
|
|
634
|
+
const responseChunks = [];
|
|
635
|
+
proxyRes.on("data", (chunk) => {
|
|
636
|
+
responseChunks.push(chunk);
|
|
637
|
+
});
|
|
638
|
+
proxyRes.on("end", async () => {
|
|
639
|
+
try {
|
|
640
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
641
|
+
const responseBodyStr = responseBody.toString("utf8");
|
|
642
|
+
const recording = {
|
|
643
|
+
request: {
|
|
644
|
+
method: req.method,
|
|
645
|
+
url: req.url,
|
|
646
|
+
headers: req.headers,
|
|
647
|
+
body: requestBody || null
|
|
648
|
+
},
|
|
649
|
+
response: {
|
|
650
|
+
statusCode: proxyRes.statusCode,
|
|
651
|
+
headers: proxyRes.headers,
|
|
652
|
+
body: responseBodyStr || null
|
|
653
|
+
},
|
|
654
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
|
+
key,
|
|
656
|
+
recordingId,
|
|
657
|
+
sequence
|
|
658
|
+
};
|
|
659
|
+
const responseHeaders = {
|
|
660
|
+
...proxyRes.headers,
|
|
661
|
+
...this.getCorsHeaders(req)
|
|
662
|
+
};
|
|
663
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
664
|
+
res.end(responseBody);
|
|
665
|
+
console.log(
|
|
666
|
+
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId}, sequence: ${sequence})`
|
|
667
|
+
);
|
|
668
|
+
resolve(recording);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error("Error completing recording:", error);
|
|
671
|
+
resolve(null);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
proxyRes.on("error", (err) => {
|
|
675
|
+
console.error("Proxy response error:", err);
|
|
676
|
+
if (!res.headersSent) {
|
|
677
|
+
this.handleProxyError(err, req, res);
|
|
678
|
+
}
|
|
679
|
+
resolve(null);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
proxyReq.on("error", (err) => {
|
|
721
684
|
this.handleProxyError(err, req, res);
|
|
685
|
+
resolve(null);
|
|
686
|
+
});
|
|
687
|
+
if (chunks.length > 0) {
|
|
688
|
+
proxyReq.write(Buffer.concat(chunks));
|
|
722
689
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
690
|
+
proxyReq.end();
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error("Error in recordAndProxyRequest:", error);
|
|
693
|
+
try {
|
|
694
|
+
this.handleProxyError(error, req, res);
|
|
695
|
+
} catch (error_) {
|
|
696
|
+
console.error("Failed to handle proxy error:", error_);
|
|
697
|
+
}
|
|
698
|
+
resolve(null);
|
|
699
|
+
}
|
|
700
|
+
})();
|
|
728
701
|
});
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
proxyReq.end();
|
|
702
|
+
this.recordingPromises.push(recordingPromise);
|
|
703
|
+
await recordingPromise;
|
|
733
704
|
}
|
|
734
705
|
handleUpgrade(req, socket, head) {
|
|
735
706
|
if (this.mode === Modes.replay) {
|
|
@@ -991,7 +962,7 @@ var playwrightProxy = {
|
|
|
991
962
|
try {
|
|
992
963
|
await setProxyMode(Modes.replay, sessionId);
|
|
993
964
|
console.log(
|
|
994
|
-
`[Cleanup] Switched to
|
|
965
|
+
`[Cleanup] Switched to replay mode for session: ${sessionId}`
|
|
995
966
|
);
|
|
996
967
|
} catch (error) {
|
|
997
968
|
console.error("[Cleanup] Error during page close cleanup:", error);
|
package/dist/index.d.cts
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;
|