From d45c2195c8b933b382cda50edf3e287fd1d38f13 Mon Sep 17 00:00:00 2001 From: Daniel Garcia Date: Fri, 25 Oct 2024 15:00:08 -0400 Subject: [PATCH] fix(fromTraffic): clone unique handler responses Repeated calls to unique request handlers would return unreadable responses that were already consumed. By cloning the response before returning, each returned response is able to be read as normal. Fixes #66 --- src/traffic/from-traffic.ts | 2 +- .../fixtures/archives/response-cloning.har | 344 ++++++++++++++++++ .../fixtures/requests/response-cloning.ts | 21 ++ test/traffic/response-cloning.test.ts | 15 + 4 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 test/traffic/fixtures/archives/response-cloning.har create mode 100644 test/traffic/fixtures/requests/response-cloning.ts create mode 100644 test/traffic/response-cloning.test.ts diff --git a/src/traffic/from-traffic.ts b/src/traffic/from-traffic.ts index b663a7b..c5213ac 100644 --- a/src/traffic/from-traffic.ts +++ b/src/traffic/from-traffic.ts @@ -69,7 +69,7 @@ export function fromTraffic( await delay(entry.time) } - return response + return isUniqueHandler ? response.clone() : response }, { once: !isUniqueHandler, diff --git a/test/traffic/fixtures/archives/response-cloning.har b/test/traffic/fixtures/archives/response-cloning.har new file mode 100644 index 0000000..aa94b8f --- /dev/null +++ b/test/traffic/fixtures/archives/response-cloning.har @@ -0,0 +1,344 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "WebInspector", + "version": "537.36" + }, + "pages": [], + "entries": [ + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "eval", + "scriptId": "13", + "url": "", + "lineNumber": 3, + "columnNumber": 39 + }, + { + "functionName": "eval", + "scriptId": "13", + "url": "", + "lineNumber": 1, + "columnNumber": 56 + }, + { + "functionName": "evaluate", + "scriptId": "6", + "url": "", + "lineNumber": 340, + "columnNumber": 15 + }, + { + "functionName": "", + "scriptId": "12", + "url": "", + "lineNumber": 0, + "columnNumber": 43 + } + ] + } + }, + "_priority": "High", + "_resourceType": "fetch", + "cache": {}, + "connection": "29", + "request": { + "method": "GET", + "url": "http://127.0.0.1:51345/resource", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:51345" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"93\", \" Not;A Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4576.0 Safari/537.36" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Origin", + "value": "null" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 477, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "X-Powered-By", + "value": "Express" + }, + { + "name": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "name": "Content-Type", + "value": "text/html; charset=utf-8" + }, + { + "name": "Content-Length", + "value": "14" + }, + { + "name": "ETag", + "value": "W/\"e-lDXCsB3mEANLxMr+ZDknI3O1vrg\"" + }, + { + "name": "Date", + "value": "Sun, 15 Aug 2021 13:41:24 GMT" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "cookies": [], + "content": { + "size": 14, + "mimeType": "text/html", + "compression": 0, + "text": "one" + }, + "redirectURL": "", + "headersSize": 236, + "bodySize": 14, + "_transferSize": 250, + "_error": null + }, + "serverIPAddress": "127.0.0.1", + "startedDateTime": "2021-08-15T13:41:24.681Z", + "time": 6.774000000096828, + "timings": { + "blocked": 0.7860000000414147, + "dns": 0.013000000000000012, + "ssl": -1, + "connect": 0.38500000000000006, + "send": 0.07999999999999996, + "wait": 5.130000000105326, + "receive": 0.37999999995008693, + "_blocked_queueing": 0.6140000000414148 + } + }, + { + "_initiator": { + "type": "script", + "stack": { + "callFrames": [ + { + "functionName": "eval", + "scriptId": "13", + "url": "", + "lineNumber": 3, + "columnNumber": 39 + }, + { + "functionName": "eval", + "scriptId": "13", + "url": "", + "lineNumber": 1, + "columnNumber": 56 + }, + { + "functionName": "evaluate", + "scriptId": "6", + "url": "", + "lineNumber": 340, + "columnNumber": 15 + }, + { + "functionName": "", + "scriptId": "12", + "url": "", + "lineNumber": 0, + "columnNumber": 43 + } + ] + } + }, + "_priority": "High", + "_resourceType": "fetch", + "cache": {}, + "connection": "34", + "request": { + "method": "GET", + "url": "http://127.0.0.1:51345/resource", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "Host", + "value": "127.0.0.1:51345" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"93\", \" Not;A Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4576.0 Safari/537.36" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"macOS\"" + }, + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Origin", + "value": "null" + }, + { + "name": "Sec-Fetch-Site", + "value": "cross-site" + }, + { + "name": "Sec-Fetch-Mode", + "value": "cors" + }, + { + "name": "Sec-Fetch-Dest", + "value": "empty" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "If-None-Match", + "value": "W/\"e-lDXCsB3mEANLxMr+ZDknI3O1vrg\"" + } + ], + "queryString": [], + "cookies": [], + "headersSize": 527, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "headers": [ + { + "name": "X-Powered-By", + "value": "Express" + }, + { + "name": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "name": "Content-Type", + "value": "text/html; charset=utf-8" + }, + { + "name": "Content-Length", + "value": "14" + }, + { + "name": "ETag", + "value": "W/\"e-3dlKK58QOHvviPbNHbANXgv8hSk\"" + }, + { + "name": "Date", + "value": "Sun, 15 Aug 2021 13:41:24 GMT" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "cookies": [], + "content": { + "size": 14, + "mimeType": "text/html", + "compression": 0, + "text": "two" + }, + "redirectURL": "", + "headersSize": 236, + "bodySize": 14, + "_transferSize": 250, + "_error": null + }, + "serverIPAddress": "127.0.0.1", + "startedDateTime": "2021-08-15T13:41:24.681Z", + "time": 7.915999999921842, + "timings": { + "blocked": 6.177999999940497, + "dns": 0.004999999999999893, + "ssl": -1, + "connect": 0.12699999999999978, + "send": 0.3100000000000005, + "wait": 1.0960000000315482, + "receive": 0.1999999999497959, + "_blocked_queueing": 0.6989999999404972 + } + } + ] + } +} diff --git a/test/traffic/fixtures/requests/response-cloning.ts b/test/traffic/fixtures/requests/response-cloning.ts new file mode 100644 index 0000000..7e1c0b4 --- /dev/null +++ b/test/traffic/fixtures/requests/response-cloning.ts @@ -0,0 +1,21 @@ +import { createTrafficScenario } from '../../utils/create-traffic-scenario' + +createTrafficScenario( + (app) => { + let isFirstRequest = true + + app.get('/resource', (req, res) => { + if (isFirstRequest) { + isFirstRequest = false + return res.send('one') + } + + res.send('two') + }) + }, + (server) => [ + // Intentionally request two same endpoints. + [server.http.url('/resource')], + [server.http.url('/resource')], + ], +) diff --git a/test/traffic/response-cloning.test.ts b/test/traffic/response-cloning.test.ts new file mode 100644 index 0000000..3769077 --- /dev/null +++ b/test/traffic/response-cloning.test.ts @@ -0,0 +1,15 @@ +import { fromTraffic } from '../../src/traffic/from-traffic.js' +import { InspectedHandler, inspectHandlers } from '../support/inspect.js' +import { _toHeaders, normalizeLocalhost, readArchive } from './utils/index.js' + +it('subsequent requests to the same endpoint should return a readable response', async () => { + const har = readArchive('test/traffic/fixtures/archives/response-cloning.har') + const handlers = fromTraffic(har, normalizeLocalhost) + await inspectHandlers(handlers) + const repeatedHandlerResponse = await handlers[handlers.length - 1]!.run({ + request: new Request('http://localhost/resource'), + requestId: crypto.randomUUID(), + }) + expect(repeatedHandlerResponse?.response).toBeDefined() + await expect(repeatedHandlerResponse!.response!.text()).resolves.toBe('two') +})