test-proxy-recorder 0.1.5 → 0.1.6
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 +334 -207
- package/dist/index.cjs +167 -31
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +167 -31
- package/dist/proxy.js +167 -31
- package/package.json +8 -3
package/dist/index.cjs
CHANGED
|
@@ -138,14 +138,17 @@ 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,
|
|
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 origin = req.headers.origin;
|
|
147
148
|
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
148
|
-
"Content-Type": "application/json"
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
151
|
+
"Access-Control-Allow-Credentials": "true"
|
|
149
152
|
});
|
|
150
153
|
}
|
|
151
154
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
@@ -201,7 +204,7 @@ var ProxyServer = class {
|
|
|
201
204
|
async switchMode(mode, id) {
|
|
202
205
|
if (this.currentSession) {
|
|
203
206
|
console.log("Switching mode, saving current session first");
|
|
204
|
-
await this.saveCurrentSession();
|
|
207
|
+
await this.saveCurrentSession(true);
|
|
205
208
|
console.log("Session saved, continuing with mode switch");
|
|
206
209
|
}
|
|
207
210
|
switch (mode) {
|
|
@@ -256,13 +259,13 @@ var ProxyServer = class {
|
|
|
256
259
|
if (timeout && timeout > 0) {
|
|
257
260
|
this.modeTimeout = setTimeout(async () => {
|
|
258
261
|
console.log("Timeout reached, switching back to transparent mode");
|
|
259
|
-
await this.saveCurrentSession();
|
|
262
|
+
await this.saveCurrentSession(true);
|
|
260
263
|
this.switchToTransparentMode();
|
|
261
264
|
this.modeTimeout = null;
|
|
262
265
|
}, timeout);
|
|
263
266
|
}
|
|
264
267
|
}
|
|
265
|
-
async saveCurrentSession() {
|
|
268
|
+
async saveCurrentSession(filterIncomplete = false) {
|
|
266
269
|
if (!this.currentSession) {
|
|
267
270
|
console.log("No current session to save");
|
|
268
271
|
return;
|
|
@@ -271,13 +274,27 @@ var ProxyServer = class {
|
|
|
271
274
|
console.log("Session has no recordings, skipping save");
|
|
272
275
|
return;
|
|
273
276
|
}
|
|
277
|
+
if (filterIncomplete) {
|
|
278
|
+
const incompleteCount = this.currentSession.recordings.filter(
|
|
279
|
+
(r) => !r.response
|
|
280
|
+
).length;
|
|
281
|
+
if (incompleteCount > 0) {
|
|
282
|
+
console.log(
|
|
283
|
+
`Removing ${incompleteCount} incomplete recording(s) without responses`
|
|
284
|
+
);
|
|
285
|
+
this.currentSession.recordings = this.currentSession.recordings.filter(
|
|
286
|
+
(r) => r.response
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
274
290
|
console.log(
|
|
275
291
|
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
276
292
|
);
|
|
277
293
|
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
278
294
|
}
|
|
279
|
-
|
|
295
|
+
saveRequestRecordSync(req, body) {
|
|
280
296
|
if (!this.currentSession) {
|
|
297
|
+
console.log("saveRequestRecordSync: No current session");
|
|
281
298
|
return;
|
|
282
299
|
}
|
|
283
300
|
const key = getReqID(req);
|
|
@@ -295,6 +312,30 @@ var ProxyServer = class {
|
|
|
295
312
|
sequence: currentSequence
|
|
296
313
|
};
|
|
297
314
|
this.currentSession.recordings.push(record);
|
|
315
|
+
console.log(
|
|
316
|
+
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
317
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${currentSequence}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
updateRequestBodySync(req, body) {
|
|
321
|
+
if (!this.currentSession) {
|
|
322
|
+
console.log("updateRequestBodySync: No current session");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const key = getReqID(req);
|
|
326
|
+
const record = this.currentSession.recordings.findLast(
|
|
327
|
+
(r) => r.key === key && !r.response
|
|
328
|
+
);
|
|
329
|
+
if (!record) {
|
|
330
|
+
console.error(
|
|
331
|
+
`updateRequestBodySync: Could not find request record for ${req.method} ${req.url}`
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
record.request.body = body || null;
|
|
336
|
+
console.log(
|
|
337
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
338
|
+
);
|
|
298
339
|
}
|
|
299
340
|
async recordResponse(req, proxyRes) {
|
|
300
341
|
if (!this.currentSession) {
|
|
@@ -323,49 +364,105 @@ var ProxyServer = class {
|
|
|
323
364
|
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
324
365
|
});
|
|
325
366
|
}
|
|
367
|
+
async recordResponseData(req, proxyRes, body) {
|
|
368
|
+
if (!this.currentSession) {
|
|
369
|
+
console.log("recordResponseData: No current session");
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const key = getReqID(req);
|
|
373
|
+
const record = this.currentSession.recordings.findLast(
|
|
374
|
+
(r) => r.key === key && !r.response
|
|
375
|
+
);
|
|
376
|
+
if (!record) {
|
|
377
|
+
const host = req.headers.host || "unknown";
|
|
378
|
+
const recordsWithKey = this.currentSession.recordings.filter(
|
|
379
|
+
(r) => r.key === key
|
|
380
|
+
);
|
|
381
|
+
console.error(
|
|
382
|
+
`Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
|
|
383
|
+
);
|
|
384
|
+
console.error(
|
|
385
|
+
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
386
|
+
);
|
|
387
|
+
console.error(
|
|
388
|
+
` Records with key:`,
|
|
389
|
+
recordsWithKey.map((r) => ({
|
|
390
|
+
seq: r.sequence,
|
|
391
|
+
hasResponse: !!r.response
|
|
392
|
+
}))
|
|
393
|
+
);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
record.response = {
|
|
397
|
+
statusCode: proxyRes.statusCode,
|
|
398
|
+
headers: proxyRes.headers,
|
|
399
|
+
body: body || null
|
|
400
|
+
};
|
|
401
|
+
await this.saveCurrentSession();
|
|
402
|
+
console.log(
|
|
403
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url}`
|
|
404
|
+
);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
326
407
|
async handleReplayRequest(req, res) {
|
|
327
408
|
const key = getReqID(req);
|
|
328
409
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
329
410
|
try {
|
|
330
411
|
const session = await loadRecordingSession(filePath);
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
(r) => r.key === key && r.
|
|
412
|
+
const host = req.headers.host || "unknown";
|
|
413
|
+
const recordsWithKey = session.recordings.filter(
|
|
414
|
+
(r) => r.key === key && r.response
|
|
334
415
|
);
|
|
335
|
-
if (
|
|
416
|
+
if (recordsWithKey.length === 0) {
|
|
336
417
|
throw new Error(
|
|
337
|
-
`No recording found for ${key}
|
|
418
|
+
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
338
419
|
);
|
|
339
420
|
}
|
|
421
|
+
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
422
|
+
const recordIndex = usageCount % recordsWithKey.length;
|
|
423
|
+
const record = recordsWithKey[recordIndex];
|
|
424
|
+
console.log(
|
|
425
|
+
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
|
|
426
|
+
);
|
|
427
|
+
this.replaySequenceMap.set(key, usageCount + 1);
|
|
340
428
|
if (!record.response) {
|
|
341
|
-
throw new Error(
|
|
429
|
+
throw new Error(
|
|
430
|
+
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
431
|
+
);
|
|
342
432
|
}
|
|
343
|
-
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
344
433
|
const { statusCode, headers, body } = record.response;
|
|
345
434
|
const origin = req.headers.origin;
|
|
346
435
|
const responseHeaders = {
|
|
347
436
|
...headers,
|
|
348
437
|
"access-control-allow-origin": origin || "*",
|
|
349
|
-
"access-control-allow-credentials": "true"
|
|
438
|
+
"access-control-allow-credentials": "true",
|
|
439
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
440
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
441
|
+
"access-control-expose-headers": "*"
|
|
350
442
|
};
|
|
351
443
|
res.writeHead(statusCode, responseHeaders);
|
|
352
444
|
res.end(body);
|
|
353
|
-
console.log(
|
|
354
|
-
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
355
|
-
);
|
|
356
445
|
} catch (error) {
|
|
357
|
-
this.handleReplayError(res, error, key, filePath);
|
|
446
|
+
this.handleReplayError(req, res, error, key, filePath);
|
|
358
447
|
}
|
|
359
448
|
}
|
|
360
|
-
handleReplayError(res, err, key, filePath) {
|
|
449
|
+
handleReplayError(req, res, err, key, filePath) {
|
|
361
450
|
const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
362
451
|
console.error("Replay error:", err);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
452
|
+
const origin = req.headers.origin;
|
|
453
|
+
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
454
|
+
"Content-Type": "application/json",
|
|
455
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
456
|
+
"Access-Control-Allow-Credentials": "true"
|
|
368
457
|
});
|
|
458
|
+
res.end(
|
|
459
|
+
JSON.stringify({
|
|
460
|
+
error: isFileNotFound ? "Recording file not found" : "Recording not found",
|
|
461
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
462
|
+
key,
|
|
463
|
+
filePath
|
|
464
|
+
})
|
|
465
|
+
);
|
|
369
466
|
}
|
|
370
467
|
async handleRequest(req, res) {
|
|
371
468
|
if (req.method === "OPTIONS") {
|
|
@@ -395,6 +492,7 @@ var ProxyServer = class {
|
|
|
395
492
|
const target = this.getTarget();
|
|
396
493
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
397
494
|
if (this.mode === Modes.record) {
|
|
495
|
+
this.saveRequestRecordSync(req, null);
|
|
398
496
|
await this.bufferAndProxyRequest(req, res, target);
|
|
399
497
|
} else {
|
|
400
498
|
this.proxy.web(req, res, { target });
|
|
@@ -405,11 +503,20 @@ var ProxyServer = class {
|
|
|
405
503
|
req.on("data", (chunk) => {
|
|
406
504
|
chunks.push(chunk);
|
|
407
505
|
});
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
506
|
+
try {
|
|
507
|
+
await new Promise((resolve, reject) => {
|
|
508
|
+
req.on("end", () => resolve());
|
|
509
|
+
req.on("error", (err) => reject(err));
|
|
510
|
+
setTimeout(
|
|
511
|
+
() => reject(new Error("Request buffering timeout")),
|
|
512
|
+
3e4
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.error("Error buffering request:", error);
|
|
517
|
+
}
|
|
411
518
|
const body = Buffer.concat(chunks).toString("utf8");
|
|
412
|
-
|
|
519
|
+
this.updateRequestBodySync(req, body);
|
|
413
520
|
const targetUrl = new URL(target);
|
|
414
521
|
const isHttps = targetUrl.protocol === "https:";
|
|
415
522
|
const requestModule = isHttps ? https__default.default : http__default.default;
|
|
@@ -424,9 +531,38 @@ var ProxyServer = class {
|
|
|
424
531
|
},
|
|
425
532
|
(proxyRes) => {
|
|
426
533
|
this.addCorsHeaders(proxyRes, req);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
534
|
+
const responseChunks = [];
|
|
535
|
+
proxyRes.on("data", (chunk) => {
|
|
536
|
+
responseChunks.push(chunk);
|
|
537
|
+
});
|
|
538
|
+
proxyRes.on("end", async () => {
|
|
539
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
540
|
+
const recorded = await this.recordResponseData(
|
|
541
|
+
req,
|
|
542
|
+
proxyRes,
|
|
543
|
+
responseBody.toString("utf8")
|
|
544
|
+
);
|
|
545
|
+
const origin = req.headers.origin;
|
|
546
|
+
const responseHeaders = {
|
|
547
|
+
...proxyRes.headers,
|
|
548
|
+
"access-control-allow-origin": origin || "*",
|
|
549
|
+
"access-control-allow-credentials": "true",
|
|
550
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
551
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
552
|
+
"access-control-expose-headers": "*"
|
|
553
|
+
};
|
|
554
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
555
|
+
res.end(responseBody);
|
|
556
|
+
if (recorded) {
|
|
557
|
+
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
proxyRes.on("error", (err) => {
|
|
561
|
+
console.error("Proxy response error:", err);
|
|
562
|
+
if (!res.headersSent) {
|
|
563
|
+
this.handleProxyError(err, req, res);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
430
566
|
}
|
|
431
567
|
);
|
|
432
568
|
proxyReq.on("error", (err) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -30,8 +30,10 @@ declare class ProxyServer {
|
|
|
30
30
|
private switchToReplayMode;
|
|
31
31
|
private setupModeTimeout;
|
|
32
32
|
private saveCurrentSession;
|
|
33
|
-
private
|
|
33
|
+
private saveRequestRecordSync;
|
|
34
|
+
private updateRequestBodySync;
|
|
34
35
|
private recordResponse;
|
|
36
|
+
private recordResponseData;
|
|
35
37
|
private handleReplayRequest;
|
|
36
38
|
private handleReplayError;
|
|
37
39
|
private handleRequest;
|
package/dist/index.d.ts
CHANGED
|
@@ -30,8 +30,10 @@ declare class ProxyServer {
|
|
|
30
30
|
private switchToReplayMode;
|
|
31
31
|
private setupModeTimeout;
|
|
32
32
|
private saveCurrentSession;
|
|
33
|
-
private
|
|
33
|
+
private saveRequestRecordSync;
|
|
34
|
+
private updateRequestBodySync;
|
|
34
35
|
private recordResponse;
|
|
36
|
+
private recordResponseData;
|
|
35
37
|
private handleReplayRequest;
|
|
36
38
|
private handleReplayError;
|
|
37
39
|
private handleRequest;
|
package/dist/index.mjs
CHANGED
|
@@ -127,14 +127,17 @@ var ProxyServer = class {
|
|
|
127
127
|
this.proxy.on("error", this.handleProxyError.bind(this));
|
|
128
128
|
this.proxy.on("proxyRes", this.handleProxyResponse.bind(this));
|
|
129
129
|
}
|
|
130
|
-
handleProxyError(err,
|
|
130
|
+
handleProxyError(err, req, res) {
|
|
131
131
|
console.error("Proxy error:", err);
|
|
132
132
|
if (!(res instanceof http.ServerResponse)) {
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
if (!res.headersSent) {
|
|
136
|
+
const origin = req.headers.origin;
|
|
136
137
|
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
137
|
-
"Content-Type": "application/json"
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
140
|
+
"Access-Control-Allow-Credentials": "true"
|
|
138
141
|
});
|
|
139
142
|
}
|
|
140
143
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
@@ -190,7 +193,7 @@ var ProxyServer = class {
|
|
|
190
193
|
async switchMode(mode, id) {
|
|
191
194
|
if (this.currentSession) {
|
|
192
195
|
console.log("Switching mode, saving current session first");
|
|
193
|
-
await this.saveCurrentSession();
|
|
196
|
+
await this.saveCurrentSession(true);
|
|
194
197
|
console.log("Session saved, continuing with mode switch");
|
|
195
198
|
}
|
|
196
199
|
switch (mode) {
|
|
@@ -245,13 +248,13 @@ var ProxyServer = class {
|
|
|
245
248
|
if (timeout && timeout > 0) {
|
|
246
249
|
this.modeTimeout = setTimeout(async () => {
|
|
247
250
|
console.log("Timeout reached, switching back to transparent mode");
|
|
248
|
-
await this.saveCurrentSession();
|
|
251
|
+
await this.saveCurrentSession(true);
|
|
249
252
|
this.switchToTransparentMode();
|
|
250
253
|
this.modeTimeout = null;
|
|
251
254
|
}, timeout);
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
|
-
async saveCurrentSession() {
|
|
257
|
+
async saveCurrentSession(filterIncomplete = false) {
|
|
255
258
|
if (!this.currentSession) {
|
|
256
259
|
console.log("No current session to save");
|
|
257
260
|
return;
|
|
@@ -260,13 +263,27 @@ var ProxyServer = class {
|
|
|
260
263
|
console.log("Session has no recordings, skipping save");
|
|
261
264
|
return;
|
|
262
265
|
}
|
|
266
|
+
if (filterIncomplete) {
|
|
267
|
+
const incompleteCount = this.currentSession.recordings.filter(
|
|
268
|
+
(r) => !r.response
|
|
269
|
+
).length;
|
|
270
|
+
if (incompleteCount > 0) {
|
|
271
|
+
console.log(
|
|
272
|
+
`Removing ${incompleteCount} incomplete recording(s) without responses`
|
|
273
|
+
);
|
|
274
|
+
this.currentSession.recordings = this.currentSession.recordings.filter(
|
|
275
|
+
(r) => r.response
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
263
279
|
console.log(
|
|
264
280
|
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
265
281
|
);
|
|
266
282
|
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
267
283
|
}
|
|
268
|
-
|
|
284
|
+
saveRequestRecordSync(req, body) {
|
|
269
285
|
if (!this.currentSession) {
|
|
286
|
+
console.log("saveRequestRecordSync: No current session");
|
|
270
287
|
return;
|
|
271
288
|
}
|
|
272
289
|
const key = getReqID(req);
|
|
@@ -284,6 +301,30 @@ var ProxyServer = class {
|
|
|
284
301
|
sequence: currentSequence
|
|
285
302
|
};
|
|
286
303
|
this.currentSession.recordings.push(record);
|
|
304
|
+
console.log(
|
|
305
|
+
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
306
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${currentSequence}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
updateRequestBodySync(req, body) {
|
|
310
|
+
if (!this.currentSession) {
|
|
311
|
+
console.log("updateRequestBodySync: No current session");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const key = getReqID(req);
|
|
315
|
+
const record = this.currentSession.recordings.findLast(
|
|
316
|
+
(r) => r.key === key && !r.response
|
|
317
|
+
);
|
|
318
|
+
if (!record) {
|
|
319
|
+
console.error(
|
|
320
|
+
`updateRequestBodySync: Could not find request record for ${req.method} ${req.url}`
|
|
321
|
+
);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
record.request.body = body || null;
|
|
325
|
+
console.log(
|
|
326
|
+
`updateRequestBodySync: Updated body for ${req.method} ${req.url} (${body.length} chars)`
|
|
327
|
+
);
|
|
287
328
|
}
|
|
288
329
|
async recordResponse(req, proxyRes) {
|
|
289
330
|
if (!this.currentSession) {
|
|
@@ -312,49 +353,105 @@ var ProxyServer = class {
|
|
|
312
353
|
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
313
354
|
});
|
|
314
355
|
}
|
|
356
|
+
async recordResponseData(req, proxyRes, body) {
|
|
357
|
+
if (!this.currentSession) {
|
|
358
|
+
console.log("recordResponseData: No current session");
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
const key = getReqID(req);
|
|
362
|
+
const record = this.currentSession.recordings.findLast(
|
|
363
|
+
(r) => r.key === key && !r.response
|
|
364
|
+
);
|
|
365
|
+
if (!record) {
|
|
366
|
+
const host = req.headers.host || "unknown";
|
|
367
|
+
const recordsWithKey = this.currentSession.recordings.filter(
|
|
368
|
+
(r) => r.key === key
|
|
369
|
+
);
|
|
370
|
+
console.error(
|
|
371
|
+
`Request record not found for response: ${key} at ${req.method} ${host}${req.url}`
|
|
372
|
+
);
|
|
373
|
+
console.error(
|
|
374
|
+
` Total recordings: ${this.currentSession.recordings.length}, with this key: ${recordsWithKey.length}`
|
|
375
|
+
);
|
|
376
|
+
console.error(
|
|
377
|
+
` Records with key:`,
|
|
378
|
+
recordsWithKey.map((r) => ({
|
|
379
|
+
seq: r.sequence,
|
|
380
|
+
hasResponse: !!r.response
|
|
381
|
+
}))
|
|
382
|
+
);
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
record.response = {
|
|
386
|
+
statusCode: proxyRes.statusCode,
|
|
387
|
+
headers: proxyRes.headers,
|
|
388
|
+
body: body || null
|
|
389
|
+
};
|
|
390
|
+
await this.saveCurrentSession();
|
|
391
|
+
console.log(
|
|
392
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url}`
|
|
393
|
+
);
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
315
396
|
async handleReplayRequest(req, res) {
|
|
316
397
|
const key = getReqID(req);
|
|
317
398
|
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
318
399
|
try {
|
|
319
400
|
const session = await loadRecordingSession(filePath);
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
(r) => r.key === key && r.
|
|
401
|
+
const host = req.headers.host || "unknown";
|
|
402
|
+
const recordsWithKey = session.recordings.filter(
|
|
403
|
+
(r) => r.key === key && r.response
|
|
323
404
|
);
|
|
324
|
-
if (
|
|
405
|
+
if (recordsWithKey.length === 0) {
|
|
325
406
|
throw new Error(
|
|
326
|
-
`No recording found for ${key}
|
|
407
|
+
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
327
408
|
);
|
|
328
409
|
}
|
|
410
|
+
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
411
|
+
const recordIndex = usageCount % recordsWithKey.length;
|
|
412
|
+
const record = recordsWithKey[recordIndex];
|
|
413
|
+
console.log(
|
|
414
|
+
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
|
|
415
|
+
);
|
|
416
|
+
this.replaySequenceMap.set(key, usageCount + 1);
|
|
329
417
|
if (!record.response) {
|
|
330
|
-
throw new Error(
|
|
418
|
+
throw new Error(
|
|
419
|
+
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
420
|
+
);
|
|
331
421
|
}
|
|
332
|
-
this.replaySequenceMap.set(key, currentSequence + 1);
|
|
333
422
|
const { statusCode, headers, body } = record.response;
|
|
334
423
|
const origin = req.headers.origin;
|
|
335
424
|
const responseHeaders = {
|
|
336
425
|
...headers,
|
|
337
426
|
"access-control-allow-origin": origin || "*",
|
|
338
|
-
"access-control-allow-credentials": "true"
|
|
427
|
+
"access-control-allow-credentials": "true",
|
|
428
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
429
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
430
|
+
"access-control-expose-headers": "*"
|
|
339
431
|
};
|
|
340
432
|
res.writeHead(statusCode, responseHeaders);
|
|
341
433
|
res.end(body);
|
|
342
|
-
console.log(
|
|
343
|
-
`Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
|
|
344
|
-
);
|
|
345
434
|
} catch (error) {
|
|
346
|
-
this.handleReplayError(res, error, key, filePath);
|
|
435
|
+
this.handleReplayError(req, res, error, key, filePath);
|
|
347
436
|
}
|
|
348
437
|
}
|
|
349
|
-
handleReplayError(res, err, key, filePath) {
|
|
438
|
+
handleReplayError(req, res, err, key, filePath) {
|
|
350
439
|
const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
351
440
|
console.error("Replay error:", err);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
441
|
+
const origin = req.headers.origin;
|
|
442
|
+
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
445
|
+
"Access-Control-Allow-Credentials": "true"
|
|
357
446
|
});
|
|
447
|
+
res.end(
|
|
448
|
+
JSON.stringify({
|
|
449
|
+
error: isFileNotFound ? "Recording file not found" : "Recording not found",
|
|
450
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
451
|
+
key,
|
|
452
|
+
filePath
|
|
453
|
+
})
|
|
454
|
+
);
|
|
358
455
|
}
|
|
359
456
|
async handleRequest(req, res) {
|
|
360
457
|
if (req.method === "OPTIONS") {
|
|
@@ -384,6 +481,7 @@ var ProxyServer = class {
|
|
|
384
481
|
const target = this.getTarget();
|
|
385
482
|
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
386
483
|
if (this.mode === Modes.record) {
|
|
484
|
+
this.saveRequestRecordSync(req, null);
|
|
387
485
|
await this.bufferAndProxyRequest(req, res, target);
|
|
388
486
|
} else {
|
|
389
487
|
this.proxy.web(req, res, { target });
|
|
@@ -394,11 +492,20 @@ var ProxyServer = class {
|
|
|
394
492
|
req.on("data", (chunk) => {
|
|
395
493
|
chunks.push(chunk);
|
|
396
494
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
495
|
+
try {
|
|
496
|
+
await new Promise((resolve, reject) => {
|
|
497
|
+
req.on("end", () => resolve());
|
|
498
|
+
req.on("error", (err) => reject(err));
|
|
499
|
+
setTimeout(
|
|
500
|
+
() => reject(new Error("Request buffering timeout")),
|
|
501
|
+
3e4
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.error("Error buffering request:", error);
|
|
506
|
+
}
|
|
400
507
|
const body = Buffer.concat(chunks).toString("utf8");
|
|
401
|
-
|
|
508
|
+
this.updateRequestBodySync(req, body);
|
|
402
509
|
const targetUrl = new URL(target);
|
|
403
510
|
const isHttps = targetUrl.protocol === "https:";
|
|
404
511
|
const requestModule = isHttps ? https : http;
|
|
@@ -413,9 +520,38 @@ var ProxyServer = class {
|
|
|
413
520
|
},
|
|
414
521
|
(proxyRes) => {
|
|
415
522
|
this.addCorsHeaders(proxyRes, req);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
523
|
+
const responseChunks = [];
|
|
524
|
+
proxyRes.on("data", (chunk) => {
|
|
525
|
+
responseChunks.push(chunk);
|
|
526
|
+
});
|
|
527
|
+
proxyRes.on("end", async () => {
|
|
528
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
529
|
+
const recorded = await this.recordResponseData(
|
|
530
|
+
req,
|
|
531
|
+
proxyRes,
|
|
532
|
+
responseBody.toString("utf8")
|
|
533
|
+
);
|
|
534
|
+
const origin = req.headers.origin;
|
|
535
|
+
const responseHeaders = {
|
|
536
|
+
...proxyRes.headers,
|
|
537
|
+
"access-control-allow-origin": origin || "*",
|
|
538
|
+
"access-control-allow-credentials": "true",
|
|
539
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
540
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
541
|
+
"access-control-expose-headers": "*"
|
|
542
|
+
};
|
|
543
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
544
|
+
res.end(responseBody);
|
|
545
|
+
if (recorded) {
|
|
546
|
+
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
proxyRes.on("error", (err) => {
|
|
550
|
+
console.error("Proxy response error:", err);
|
|
551
|
+
if (!res.headersSent) {
|
|
552
|
+
this.handleProxyError(err, req, res);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
419
555
|
}
|
|
420
556
|
);
|
|
421
557
|
proxyReq.on("error", (err) => {
|