test-proxy-recorder 0.1.6 → 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 +63 -56
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +63 -56
- package/dist/proxy.js +63 -56
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -144,11 +144,10 @@ var ProxyServer = class {
|
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
if (!res.headersSent) {
|
|
147
|
-
const
|
|
147
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
148
148
|
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
149
149
|
"Content-Type": "application/json",
|
|
150
|
-
|
|
151
|
-
"Access-Control-Allow-Credentials": "true"
|
|
150
|
+
...corsHeaders
|
|
152
151
|
});
|
|
153
152
|
}
|
|
154
153
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
@@ -159,24 +158,51 @@ var ProxyServer = class {
|
|
|
159
158
|
this.recordResponse(req, proxyRes);
|
|
160
159
|
}
|
|
161
160
|
}
|
|
162
|
-
|
|
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) {
|
|
163
167
|
const origin = req.headers.origin;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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);
|
|
169
179
|
}
|
|
170
180
|
getTarget() {
|
|
171
181
|
const target = this.targets[this.currentTargetIndex];
|
|
172
182
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
173
183
|
return target;
|
|
174
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
|
+
}
|
|
175
196
|
async handleControlRequest(req, res) {
|
|
176
197
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
206
|
const { mode, id, timeout: requestTimeout } = data;
|
|
181
207
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
182
208
|
this.clearModeTimeout();
|
|
@@ -270,10 +296,6 @@ var ProxyServer = class {
|
|
|
270
296
|
console.log("No current session to save");
|
|
271
297
|
return;
|
|
272
298
|
}
|
|
273
|
-
if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
|
|
274
|
-
console.log("Session has no recordings, skipping save");
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
299
|
if (filterIncomplete) {
|
|
278
300
|
const incompleteCount = this.currentSession.recordings.filter(
|
|
279
301
|
(r) => !r.response
|
|
@@ -298,8 +320,6 @@ var ProxyServer = class {
|
|
|
298
320
|
return;
|
|
299
321
|
}
|
|
300
322
|
const key = getReqID(req);
|
|
301
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
302
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
303
323
|
const record = {
|
|
304
324
|
request: {
|
|
305
325
|
method: req.method,
|
|
@@ -309,12 +329,13 @@ var ProxyServer = class {
|
|
|
309
329
|
},
|
|
310
330
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
311
331
|
key,
|
|
312
|
-
sequence:
|
|
332
|
+
sequence: -1
|
|
333
|
+
// Temporary, will be set when response arrives
|
|
313
334
|
};
|
|
314
335
|
this.currentSession.recordings.push(record);
|
|
315
336
|
console.log(
|
|
316
337
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
317
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key},
|
|
338
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
318
339
|
);
|
|
319
340
|
}
|
|
320
341
|
updateRequestBodySync(req, body) {
|
|
@@ -360,7 +381,6 @@ var ProxyServer = class {
|
|
|
360
381
|
headers: proxyRes.headers,
|
|
361
382
|
body: body || null
|
|
362
383
|
};
|
|
363
|
-
await this.saveCurrentSession();
|
|
364
384
|
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
365
385
|
});
|
|
366
386
|
}
|
|
@@ -398,9 +418,12 @@ var ProxyServer = class {
|
|
|
398
418
|
headers: proxyRes.headers,
|
|
399
419
|
body: body || null
|
|
400
420
|
};
|
|
401
|
-
|
|
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);
|
|
402
425
|
console.log(
|
|
403
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url}`
|
|
426
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
404
427
|
);
|
|
405
428
|
return true;
|
|
406
429
|
}
|
|
@@ -410,19 +433,22 @@ var ProxyServer = class {
|
|
|
410
433
|
try {
|
|
411
434
|
const session = await loadRecordingSession(filePath);
|
|
412
435
|
const host = req.headers.host || "unknown";
|
|
413
|
-
const recordsWithKey = session.recordings.filter(
|
|
414
|
-
(r) => r.key === key && r.response
|
|
415
|
-
);
|
|
436
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
416
437
|
if (recordsWithKey.length === 0) {
|
|
417
438
|
throw new Error(
|
|
418
439
|
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
419
440
|
);
|
|
420
441
|
}
|
|
421
442
|
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
|
424
450
|
console.log(
|
|
425
|
-
`Replaying ${req.method} ${req.url} (usage: ${usageCount},
|
|
451
|
+
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
|
426
452
|
);
|
|
427
453
|
this.replaySequenceMap.set(key, usageCount + 1);
|
|
428
454
|
if (!record.response) {
|
|
@@ -431,14 +457,9 @@ var ProxyServer = class {
|
|
|
431
457
|
);
|
|
432
458
|
}
|
|
433
459
|
const { statusCode, headers, body } = record.response;
|
|
434
|
-
const origin = req.headers.origin;
|
|
435
460
|
const responseHeaders = {
|
|
436
461
|
...headers,
|
|
437
|
-
|
|
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": "*"
|
|
462
|
+
...this.getCorsHeaders(req)
|
|
442
463
|
};
|
|
443
464
|
res.writeHead(statusCode, responseHeaders);
|
|
444
465
|
res.end(body);
|
|
@@ -449,11 +470,10 @@ var ProxyServer = class {
|
|
|
449
470
|
handleReplayError(req, res, err, key, filePath) {
|
|
450
471
|
const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
451
472
|
console.error("Replay error:", err);
|
|
452
|
-
const
|
|
473
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
453
474
|
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
454
475
|
"Content-Type": "application/json",
|
|
455
|
-
|
|
456
|
-
"Access-Control-Allow-Credentials": "true"
|
|
476
|
+
...corsHeaders
|
|
457
477
|
});
|
|
458
478
|
res.end(
|
|
459
479
|
JSON.stringify({
|
|
@@ -468,7 +488,8 @@ var ProxyServer = class {
|
|
|
468
488
|
if (req.method === "OPTIONS") {
|
|
469
489
|
return this.handleCorsPreflightRequest(req, res);
|
|
470
490
|
}
|
|
471
|
-
|
|
491
|
+
const urlPath = req.url?.split("?")[0] || "";
|
|
492
|
+
if (urlPath === CONTROL_ENDPOINT) {
|
|
472
493
|
return this.handleControlRequest(req, res);
|
|
473
494
|
}
|
|
474
495
|
if (this.mode === Modes.replay) {
|
|
@@ -477,12 +498,9 @@ var ProxyServer = class {
|
|
|
477
498
|
await this.handleProxyRequest(req, res);
|
|
478
499
|
}
|
|
479
500
|
handleCorsPreflightRequest(req, res) {
|
|
480
|
-
const
|
|
501
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
481
502
|
res.writeHead(HTTP_STATUS_OK, {
|
|
482
|
-
|
|
483
|
-
"Access-Control-Allow-Credentials": "true",
|
|
484
|
-
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
485
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
503
|
+
...corsHeaders,
|
|
486
504
|
"Access-Control-Max-Age": "86400"
|
|
487
505
|
// 24 hours
|
|
488
506
|
});
|
|
@@ -542,14 +560,9 @@ var ProxyServer = class {
|
|
|
542
560
|
proxyRes,
|
|
543
561
|
responseBody.toString("utf8")
|
|
544
562
|
);
|
|
545
|
-
const origin = req.headers.origin;
|
|
546
563
|
const responseHeaders = {
|
|
547
564
|
...proxyRes.headers,
|
|
548
|
-
|
|
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": "*"
|
|
565
|
+
...this.getCorsHeaders(req)
|
|
553
566
|
};
|
|
554
567
|
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
555
568
|
res.end(responseBody);
|
|
@@ -614,9 +627,6 @@ var ProxyServer = class {
|
|
|
614
627
|
if (backendWs.readyState === ws.WebSocket.OPEN) {
|
|
615
628
|
backendWs.send(message);
|
|
616
629
|
}
|
|
617
|
-
this.saveCurrentSession().catch((error) => {
|
|
618
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
619
|
-
});
|
|
620
630
|
});
|
|
621
631
|
backendWs.on("message", (data) => {
|
|
622
632
|
const message = data.toString();
|
|
@@ -628,9 +638,6 @@ var ProxyServer = class {
|
|
|
628
638
|
if (clientWs.readyState === ws.WebSocket.OPEN) {
|
|
629
639
|
clientWs.send(message);
|
|
630
640
|
}
|
|
631
|
-
this.saveCurrentSession().catch((error) => {
|
|
632
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
633
|
-
});
|
|
634
641
|
});
|
|
635
642
|
clientWs.on("error", (err) => {
|
|
636
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;
|
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;
|
package/dist/index.mjs
CHANGED
|
@@ -133,11 +133,10 @@ var ProxyServer = class {
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
if (!res.headersSent) {
|
|
136
|
-
const
|
|
136
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
137
137
|
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
138
138
|
"Content-Type": "application/json",
|
|
139
|
-
|
|
140
|
-
"Access-Control-Allow-Credentials": "true"
|
|
139
|
+
...corsHeaders
|
|
141
140
|
});
|
|
142
141
|
}
|
|
143
142
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
@@ -148,24 +147,51 @@ var ProxyServer = class {
|
|
|
148
147
|
this.recordResponse(req, proxyRes);
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Get CORS headers for a given request
|
|
152
|
+
* @param req The incoming HTTP request
|
|
153
|
+
* @returns An object containing CORS headers
|
|
154
|
+
*/
|
|
155
|
+
getCorsHeaders(req) {
|
|
152
156
|
const origin = req.headers.origin;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
return {
|
|
158
|
+
"access-control-allow-origin": origin || "*",
|
|
159
|
+
"access-control-allow-credentials": "true",
|
|
160
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
161
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
162
|
+
"access-control-expose-headers": "*"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
addCorsHeaders(proxyRes, req) {
|
|
166
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
167
|
+
Object.assign(proxyRes.headers, corsHeaders);
|
|
158
168
|
}
|
|
159
169
|
getTarget() {
|
|
160
170
|
const target = this.targets[this.currentTargetIndex];
|
|
161
171
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
162
172
|
return target;
|
|
163
173
|
}
|
|
174
|
+
parseGetParams(req) {
|
|
175
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
176
|
+
const mode = url.searchParams.get("mode");
|
|
177
|
+
const id = url.searchParams.get("id") || void 0;
|
|
178
|
+
const timeoutParam = url.searchParams.get("timeout");
|
|
179
|
+
const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
|
|
180
|
+
if (!mode) {
|
|
181
|
+
throw new Error("Mode parameter is required");
|
|
182
|
+
}
|
|
183
|
+
return { mode, id, timeout };
|
|
184
|
+
}
|
|
164
185
|
async handleControlRequest(req, res) {
|
|
165
186
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
let data;
|
|
188
|
+
if (req.method === "GET") {
|
|
189
|
+
data = this.parseGetParams(req);
|
|
190
|
+
} else {
|
|
191
|
+
const body = await readRequestBody(req);
|
|
192
|
+
console.log("MODE CHANGE (POST)", body);
|
|
193
|
+
data = JSON.parse(body);
|
|
194
|
+
}
|
|
169
195
|
const { mode, id, timeout: requestTimeout } = data;
|
|
170
196
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
171
197
|
this.clearModeTimeout();
|
|
@@ -259,10 +285,6 @@ var ProxyServer = class {
|
|
|
259
285
|
console.log("No current session to save");
|
|
260
286
|
return;
|
|
261
287
|
}
|
|
262
|
-
if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
|
|
263
|
-
console.log("Session has no recordings, skipping save");
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
288
|
if (filterIncomplete) {
|
|
267
289
|
const incompleteCount = this.currentSession.recordings.filter(
|
|
268
290
|
(r) => !r.response
|
|
@@ -287,8 +309,6 @@ var ProxyServer = class {
|
|
|
287
309
|
return;
|
|
288
310
|
}
|
|
289
311
|
const key = getReqID(req);
|
|
290
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
291
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
292
312
|
const record = {
|
|
293
313
|
request: {
|
|
294
314
|
method: req.method,
|
|
@@ -298,12 +318,13 @@ var ProxyServer = class {
|
|
|
298
318
|
},
|
|
299
319
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
300
320
|
key,
|
|
301
|
-
sequence:
|
|
321
|
+
sequence: -1
|
|
322
|
+
// Temporary, will be set when response arrives
|
|
302
323
|
};
|
|
303
324
|
this.currentSession.recordings.push(record);
|
|
304
325
|
console.log(
|
|
305
326
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
306
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key},
|
|
327
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
307
328
|
);
|
|
308
329
|
}
|
|
309
330
|
updateRequestBodySync(req, body) {
|
|
@@ -349,7 +370,6 @@ var ProxyServer = class {
|
|
|
349
370
|
headers: proxyRes.headers,
|
|
350
371
|
body: body || null
|
|
351
372
|
};
|
|
352
|
-
await this.saveCurrentSession();
|
|
353
373
|
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
354
374
|
});
|
|
355
375
|
}
|
|
@@ -387,9 +407,12 @@ var ProxyServer = class {
|
|
|
387
407
|
headers: proxyRes.headers,
|
|
388
408
|
body: body || null
|
|
389
409
|
};
|
|
390
|
-
|
|
410
|
+
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
411
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
412
|
+
record.sequence = currentSequence;
|
|
413
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
391
414
|
console.log(
|
|
392
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url}`
|
|
415
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
393
416
|
);
|
|
394
417
|
return true;
|
|
395
418
|
}
|
|
@@ -399,19 +422,22 @@ var ProxyServer = class {
|
|
|
399
422
|
try {
|
|
400
423
|
const session = await loadRecordingSession(filePath);
|
|
401
424
|
const host = req.headers.host || "unknown";
|
|
402
|
-
const recordsWithKey = session.recordings.filter(
|
|
403
|
-
(r) => r.key === key && r.response
|
|
404
|
-
);
|
|
425
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
405
426
|
if (recordsWithKey.length === 0) {
|
|
406
427
|
throw new Error(
|
|
407
428
|
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
408
429
|
);
|
|
409
430
|
}
|
|
410
431
|
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
411
|
-
|
|
412
|
-
|
|
432
|
+
let record;
|
|
433
|
+
if (recordsWithKey.length > 1) {
|
|
434
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
435
|
+
} else {
|
|
436
|
+
const recordIndex = usageCount % recordsWithKey.length;
|
|
437
|
+
record = recordsWithKey[recordIndex];
|
|
438
|
+
}
|
|
413
439
|
console.log(
|
|
414
|
-
`Replaying ${req.method} ${req.url} (usage: ${usageCount},
|
|
440
|
+
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
|
415
441
|
);
|
|
416
442
|
this.replaySequenceMap.set(key, usageCount + 1);
|
|
417
443
|
if (!record.response) {
|
|
@@ -420,14 +446,9 @@ var ProxyServer = class {
|
|
|
420
446
|
);
|
|
421
447
|
}
|
|
422
448
|
const { statusCode, headers, body } = record.response;
|
|
423
|
-
const origin = req.headers.origin;
|
|
424
449
|
const responseHeaders = {
|
|
425
450
|
...headers,
|
|
426
|
-
|
|
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": "*"
|
|
451
|
+
...this.getCorsHeaders(req)
|
|
431
452
|
};
|
|
432
453
|
res.writeHead(statusCode, responseHeaders);
|
|
433
454
|
res.end(body);
|
|
@@ -438,11 +459,10 @@ var ProxyServer = class {
|
|
|
438
459
|
handleReplayError(req, res, err, key, filePath) {
|
|
439
460
|
const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
440
461
|
console.error("Replay error:", err);
|
|
441
|
-
const
|
|
462
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
442
463
|
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
443
464
|
"Content-Type": "application/json",
|
|
444
|
-
|
|
445
|
-
"Access-Control-Allow-Credentials": "true"
|
|
465
|
+
...corsHeaders
|
|
446
466
|
});
|
|
447
467
|
res.end(
|
|
448
468
|
JSON.stringify({
|
|
@@ -457,7 +477,8 @@ var ProxyServer = class {
|
|
|
457
477
|
if (req.method === "OPTIONS") {
|
|
458
478
|
return this.handleCorsPreflightRequest(req, res);
|
|
459
479
|
}
|
|
460
|
-
|
|
480
|
+
const urlPath = req.url?.split("?")[0] || "";
|
|
481
|
+
if (urlPath === CONTROL_ENDPOINT) {
|
|
461
482
|
return this.handleControlRequest(req, res);
|
|
462
483
|
}
|
|
463
484
|
if (this.mode === Modes.replay) {
|
|
@@ -466,12 +487,9 @@ var ProxyServer = class {
|
|
|
466
487
|
await this.handleProxyRequest(req, res);
|
|
467
488
|
}
|
|
468
489
|
handleCorsPreflightRequest(req, res) {
|
|
469
|
-
const
|
|
490
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
470
491
|
res.writeHead(HTTP_STATUS_OK, {
|
|
471
|
-
|
|
472
|
-
"Access-Control-Allow-Credentials": "true",
|
|
473
|
-
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
474
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
492
|
+
...corsHeaders,
|
|
475
493
|
"Access-Control-Max-Age": "86400"
|
|
476
494
|
// 24 hours
|
|
477
495
|
});
|
|
@@ -531,14 +549,9 @@ var ProxyServer = class {
|
|
|
531
549
|
proxyRes,
|
|
532
550
|
responseBody.toString("utf8")
|
|
533
551
|
);
|
|
534
|
-
const origin = req.headers.origin;
|
|
535
552
|
const responseHeaders = {
|
|
536
553
|
...proxyRes.headers,
|
|
537
|
-
|
|
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": "*"
|
|
554
|
+
...this.getCorsHeaders(req)
|
|
542
555
|
};
|
|
543
556
|
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
544
557
|
res.end(responseBody);
|
|
@@ -603,9 +616,6 @@ var ProxyServer = class {
|
|
|
603
616
|
if (backendWs.readyState === WebSocket.OPEN) {
|
|
604
617
|
backendWs.send(message);
|
|
605
618
|
}
|
|
606
|
-
this.saveCurrentSession().catch((error) => {
|
|
607
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
608
|
-
});
|
|
609
619
|
});
|
|
610
620
|
backendWs.on("message", (data) => {
|
|
611
621
|
const message = data.toString();
|
|
@@ -617,9 +627,6 @@ var ProxyServer = class {
|
|
|
617
627
|
if (clientWs.readyState === WebSocket.OPEN) {
|
|
618
628
|
clientWs.send(message);
|
|
619
629
|
}
|
|
620
|
-
this.saveCurrentSession().catch((error) => {
|
|
621
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
622
|
-
});
|
|
623
630
|
});
|
|
624
631
|
clientWs.on("error", (err) => {
|
|
625
632
|
console.error("Client WebSocket error:", err);
|
package/dist/proxy.js
CHANGED
|
@@ -167,11 +167,10 @@ var ProxyServer = class {
|
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
if (!res.headersSent) {
|
|
170
|
-
const
|
|
170
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
171
171
|
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
172
172
|
"Content-Type": "application/json",
|
|
173
|
-
|
|
174
|
-
"Access-Control-Allow-Credentials": "true"
|
|
173
|
+
...corsHeaders
|
|
175
174
|
});
|
|
176
175
|
}
|
|
177
176
|
res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
|
|
@@ -182,24 +181,51 @@ var ProxyServer = class {
|
|
|
182
181
|
this.recordResponse(req, proxyRes);
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Get CORS headers for a given request
|
|
186
|
+
* @param req The incoming HTTP request
|
|
187
|
+
* @returns An object containing CORS headers
|
|
188
|
+
*/
|
|
189
|
+
getCorsHeaders(req) {
|
|
186
190
|
const origin = req.headers.origin;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
return {
|
|
192
|
+
"access-control-allow-origin": origin || "*",
|
|
193
|
+
"access-control-allow-credentials": "true",
|
|
194
|
+
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
195
|
+
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
196
|
+
"access-control-expose-headers": "*"
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
addCorsHeaders(proxyRes, req) {
|
|
200
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
201
|
+
Object.assign(proxyRes.headers, corsHeaders);
|
|
192
202
|
}
|
|
193
203
|
getTarget() {
|
|
194
204
|
const target = this.targets[this.currentTargetIndex];
|
|
195
205
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
196
206
|
return target;
|
|
197
207
|
}
|
|
208
|
+
parseGetParams(req) {
|
|
209
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
210
|
+
const mode = url.searchParams.get("mode");
|
|
211
|
+
const id = url.searchParams.get("id") || void 0;
|
|
212
|
+
const timeoutParam = url.searchParams.get("timeout");
|
|
213
|
+
const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
|
|
214
|
+
if (!mode) {
|
|
215
|
+
throw new Error("Mode parameter is required");
|
|
216
|
+
}
|
|
217
|
+
return { mode, id, timeout };
|
|
218
|
+
}
|
|
198
219
|
async handleControlRequest(req, res) {
|
|
199
220
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
221
|
+
let data;
|
|
222
|
+
if (req.method === "GET") {
|
|
223
|
+
data = this.parseGetParams(req);
|
|
224
|
+
} else {
|
|
225
|
+
const body = await readRequestBody(req);
|
|
226
|
+
console.log("MODE CHANGE (POST)", body);
|
|
227
|
+
data = JSON.parse(body);
|
|
228
|
+
}
|
|
203
229
|
const { mode, id, timeout: requestTimeout } = data;
|
|
204
230
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
205
231
|
this.clearModeTimeout();
|
|
@@ -293,10 +319,6 @@ var ProxyServer = class {
|
|
|
293
319
|
console.log("No current session to save");
|
|
294
320
|
return;
|
|
295
321
|
}
|
|
296
|
-
if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
|
|
297
|
-
console.log("Session has no recordings, skipping save");
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
322
|
if (filterIncomplete) {
|
|
301
323
|
const incompleteCount = this.currentSession.recordings.filter(
|
|
302
324
|
(r) => !r.response
|
|
@@ -321,8 +343,6 @@ var ProxyServer = class {
|
|
|
321
343
|
return;
|
|
322
344
|
}
|
|
323
345
|
const key = getReqID(req);
|
|
324
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
325
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
326
346
|
const record = {
|
|
327
347
|
request: {
|
|
328
348
|
method: req.method,
|
|
@@ -332,12 +352,13 @@ var ProxyServer = class {
|
|
|
332
352
|
},
|
|
333
353
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
334
354
|
key,
|
|
335
|
-
sequence:
|
|
355
|
+
sequence: -1
|
|
356
|
+
// Temporary, will be set when response arrives
|
|
336
357
|
};
|
|
337
358
|
this.currentSession.recordings.push(record);
|
|
338
359
|
console.log(
|
|
339
360
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
340
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key},
|
|
361
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
341
362
|
);
|
|
342
363
|
}
|
|
343
364
|
updateRequestBodySync(req, body) {
|
|
@@ -383,7 +404,6 @@ var ProxyServer = class {
|
|
|
383
404
|
headers: proxyRes.headers,
|
|
384
405
|
body: body || null
|
|
385
406
|
};
|
|
386
|
-
await this.saveCurrentSession();
|
|
387
407
|
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
388
408
|
});
|
|
389
409
|
}
|
|
@@ -421,9 +441,12 @@ var ProxyServer = class {
|
|
|
421
441
|
headers: proxyRes.headers,
|
|
422
442
|
body: body || null
|
|
423
443
|
};
|
|
424
|
-
|
|
444
|
+
record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
445
|
+
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
446
|
+
record.sequence = currentSequence;
|
|
447
|
+
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
425
448
|
console.log(
|
|
426
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url}`
|
|
449
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
|
|
427
450
|
);
|
|
428
451
|
return true;
|
|
429
452
|
}
|
|
@@ -433,19 +456,22 @@ var ProxyServer = class {
|
|
|
433
456
|
try {
|
|
434
457
|
const session = await loadRecordingSession(filePath);
|
|
435
458
|
const host = req.headers.host || "unknown";
|
|
436
|
-
const recordsWithKey = session.recordings.filter(
|
|
437
|
-
(r) => r.key === key && r.response
|
|
438
|
-
);
|
|
459
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
|
|
439
460
|
if (recordsWithKey.length === 0) {
|
|
440
461
|
throw new Error(
|
|
441
462
|
`No recording found for ${key} at ${req.method} ${host}${req.url}`
|
|
442
463
|
);
|
|
443
464
|
}
|
|
444
465
|
const usageCount = this.replaySequenceMap.get(key) || 0;
|
|
445
|
-
|
|
446
|
-
|
|
466
|
+
let record;
|
|
467
|
+
if (recordsWithKey.length > 1) {
|
|
468
|
+
record = recordsWithKey[recordsWithKey.length - 1];
|
|
469
|
+
} else {
|
|
470
|
+
const recordIndex = usageCount % recordsWithKey.length;
|
|
471
|
+
record = recordsWithKey[recordIndex];
|
|
472
|
+
}
|
|
447
473
|
console.log(
|
|
448
|
-
`Replaying ${req.method} ${req.url} (usage: ${usageCount},
|
|
474
|
+
`Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
|
|
449
475
|
);
|
|
450
476
|
this.replaySequenceMap.set(key, usageCount + 1);
|
|
451
477
|
if (!record.response) {
|
|
@@ -454,14 +480,9 @@ var ProxyServer = class {
|
|
|
454
480
|
);
|
|
455
481
|
}
|
|
456
482
|
const { statusCode, headers, body } = record.response;
|
|
457
|
-
const origin = req.headers.origin;
|
|
458
483
|
const responseHeaders = {
|
|
459
484
|
...headers,
|
|
460
|
-
|
|
461
|
-
"access-control-allow-credentials": "true",
|
|
462
|
-
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
463
|
-
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
464
|
-
"access-control-expose-headers": "*"
|
|
485
|
+
...this.getCorsHeaders(req)
|
|
465
486
|
};
|
|
466
487
|
res.writeHead(statusCode, responseHeaders);
|
|
467
488
|
res.end(body);
|
|
@@ -472,11 +493,10 @@ var ProxyServer = class {
|
|
|
472
493
|
handleReplayError(req, res, err, key, filePath) {
|
|
473
494
|
const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
474
495
|
console.error("Replay error:", err);
|
|
475
|
-
const
|
|
496
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
476
497
|
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
477
498
|
"Content-Type": "application/json",
|
|
478
|
-
|
|
479
|
-
"Access-Control-Allow-Credentials": "true"
|
|
499
|
+
...corsHeaders
|
|
480
500
|
});
|
|
481
501
|
res.end(
|
|
482
502
|
JSON.stringify({
|
|
@@ -491,7 +511,8 @@ var ProxyServer = class {
|
|
|
491
511
|
if (req.method === "OPTIONS") {
|
|
492
512
|
return this.handleCorsPreflightRequest(req, res);
|
|
493
513
|
}
|
|
494
|
-
|
|
514
|
+
const urlPath = req.url?.split("?")[0] || "";
|
|
515
|
+
if (urlPath === CONTROL_ENDPOINT) {
|
|
495
516
|
return this.handleControlRequest(req, res);
|
|
496
517
|
}
|
|
497
518
|
if (this.mode === Modes.replay) {
|
|
@@ -500,12 +521,9 @@ var ProxyServer = class {
|
|
|
500
521
|
await this.handleProxyRequest(req, res);
|
|
501
522
|
}
|
|
502
523
|
handleCorsPreflightRequest(req, res) {
|
|
503
|
-
const
|
|
524
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
504
525
|
res.writeHead(HTTP_STATUS_OK, {
|
|
505
|
-
|
|
506
|
-
"Access-Control-Allow-Credentials": "true",
|
|
507
|
-
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
508
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
526
|
+
...corsHeaders,
|
|
509
527
|
"Access-Control-Max-Age": "86400"
|
|
510
528
|
// 24 hours
|
|
511
529
|
});
|
|
@@ -565,14 +583,9 @@ var ProxyServer = class {
|
|
|
565
583
|
proxyRes,
|
|
566
584
|
responseBody.toString("utf8")
|
|
567
585
|
);
|
|
568
|
-
const origin = req.headers.origin;
|
|
569
586
|
const responseHeaders = {
|
|
570
587
|
...proxyRes.headers,
|
|
571
|
-
|
|
572
|
-
"access-control-allow-credentials": "true",
|
|
573
|
-
"access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
|
|
574
|
-
"access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
575
|
-
"access-control-expose-headers": "*"
|
|
588
|
+
...this.getCorsHeaders(req)
|
|
576
589
|
};
|
|
577
590
|
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
578
591
|
res.end(responseBody);
|
|
@@ -637,9 +650,6 @@ var ProxyServer = class {
|
|
|
637
650
|
if (backendWs.readyState === WebSocket.OPEN) {
|
|
638
651
|
backendWs.send(message);
|
|
639
652
|
}
|
|
640
|
-
this.saveCurrentSession().catch((error) => {
|
|
641
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
642
|
-
});
|
|
643
653
|
});
|
|
644
654
|
backendWs.on("message", (data) => {
|
|
645
655
|
const message = data.toString();
|
|
@@ -651,9 +661,6 @@ var ProxyServer = class {
|
|
|
651
661
|
if (clientWs.readyState === WebSocket.OPEN) {
|
|
652
662
|
clientWs.send(message);
|
|
653
663
|
}
|
|
654
|
-
this.saveCurrentSession().catch((error) => {
|
|
655
|
-
console.error("Failed to save WebSocket recording:", error);
|
|
656
|
-
});
|
|
657
664
|
});
|
|
658
665
|
clientWs.on("error", (err) => {
|
|
659
666
|
console.error("Client WebSocket error:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-proxy-recorder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|