diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index 9e9dc5d6e..93ad76493 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -43,7 +43,11 @@ const STATIC_API_HINTS = { 'res.getHeaders()', 'res.getBody()', 'res.setBody(data)', - 'res.getResponseTime()' + 'res.getResponseTime()', + 'res.getSize()', + 'res.getSize().header', + 'res.getSize().body', + 'res.getSize().total', ], bru: [ 'bru', diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index a7a0c3ca3..49ff39b20 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -415,8 +415,9 @@ const runSingleRequest = async function ( /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); - const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); + const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); response.data = data; + response.dataBuffer = dataBuffer; // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); @@ -428,8 +429,9 @@ const runSingleRequest = async function ( } } catch (err) { if (err?.response) { - const { data } = parseDataFromResponse(err?.response); + const { data, dataBuffer } = parseDataFromResponse(err?.response); err.response.data = data; + err.response.dataBuffer = dataBuffer; response = err.response; // Prevents the duration on leaking to the actual result diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index 252c4c2d3..9dc48a5cd 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -26,6 +26,11 @@ const replacements = { 'pm\\.response\\.headers\\.get\\(': 'res.getHeader(', 'pm\\.response\\.to\\.have\\.body\\(': 'expect(res.getBody()).to.equal(', 'pm\\.response\\.to\\.have\\.header\\(': 'expect(res.getHeaders()).to.have.property(', + 'pm\\.response\\.size\\(\\)': 'res.getSize()', + 'pm\\.response\\.size\\(\\)\\.body': 'res.getSize().body', + 'pm\\.response\\.responseSize': 'res.getSize().body', + 'pm\\.response\\.size\\(\\)\\.header': 'res.getSize().header', + 'pm\\.response\\.size\\(\\)\\.total': 'res.getSize().total', 'pm\\.environment\\.name': 'bru.getEnvName()', 'pm\\.response\\.status': 'res.statusText', 'pm\\.response\\.headers': 'res.getHeaders()', diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 4d68c2b68..6b0cea683 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -84,6 +84,11 @@ const simpleTranslations = { 'pm.response.responseTime': 'res.getResponseTime()', 'pm.response.statusText': 'res.statusText', 'pm.response.headers': 'res.getHeaders()', + 'pm.response.size': 'res.getSize', + 'pm.response.responseSize': 'res.getSize().body', + 'pm.response.size().body': 'res.getSize().body', + 'pm.response.size().header': 'res.getSize().header', + 'pm.response.size().total': 'res.getSize().total', // Execution control 'pm.execution.skipRequest': 'bru.runner.skipRequest', diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js index a57b8435a..590825f13 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js @@ -9,6 +9,11 @@ describe('postmanTranslations - response commands', () => { const responseJson = pm.response.json(); const responseStatus = pm.response.status; const responseHeaders = pm.response.headers; + const responseSize = pm.response.size(); + const responseSizeBody = pm.response.size().body; + const responseSizeHeader = pm.response.size().header; + const responseSizeTotal = pm.response.size().total; + const responseSizeBody2 = pm.response.responseSize; pm.test('Status code is 200', function() { pm.response.to.have.status(200); @@ -21,6 +26,11 @@ describe('postmanTranslations - response commands', () => { const responseJson = res.getBody(); const responseStatus = res.statusText; const responseHeaders = res.getHeaders(); + const responseSize = res.getSize(); + const responseSizeBody = res.getSize().body; + const responseSizeHeader = res.getSize().header; + const responseSizeTotal = res.getSize().total; + const responseSizeBody2 = res.getSize().body; test('Status code is 200', function() { expect(res.getStatus()).to.equal(200); @@ -29,6 +39,3 @@ describe('postmanTranslations - response commands', () => { expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); }); - - - diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js index 3a1e45dbc..5df2fe5bb 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js @@ -553,4 +553,35 @@ describe('Response Translation', () => { expect(res.getBody()).to.equal({"status": "ok"}); `); }); + + // --- getSize translations --------------------------- + it('should translate pm.response.size()', () => { + const code = 'const size = pm.response.size();'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const size = res.getSize();'); + }); + + it('should translate pm.response.size().body', () => { + const code = 'const bodySize = pm.response.size().body;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const bodySize = res.getSize().body;'); + }); + + it('should translate pm.response.size().header', () => { + const code = 'const headerSize = pm.response.size().header;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const headerSize = res.getSize().header;'); + }); + + it('should translate pm.response.size().total', () => { + const code = 'const totalSize = pm.response.size().total;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const totalSize = res.getSize().total;'); + }); + + it('should translate pm.response.responseSize alias', () => { + const code = 'const responseSize = pm.response.responseSize;'; + const translatedCode = translateCode(code); + expect(translatedCode).toBe('const responseSize = res.getSize().body;'); + }); }); \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dc45b86ec..21ae36d2b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -876,8 +876,8 @@ const registerNetworkIpc = (mainWindow) => { statusText: response.statusText, headers: response.headers, data: response.data, - dataBuffer: dataBuffer.toString('base64'), - size: Buffer.byteLength(dataBuffer), + dataBuffer: response.dataBuffer.toString('base64'), + size: Buffer.byteLength(response.dataBuffer), duration: responseTime ?? 0, timeline: response.timeline }; @@ -1153,6 +1153,7 @@ const registerNetworkIpc = (mainWindow) => { response.data = data; response.dataBuffer = dataBuffer; response.responseTime = response.headers.get('request-duration'); + response.headers.delete('request-duration'); // save cookies if (preferencesUtil.shouldStoreCookies()) { @@ -1174,8 +1175,8 @@ const registerNetworkIpc = (mainWindow) => { dataBuffer: dataBuffer.toString('base64'), size: Buffer.byteLength(dataBuffer), data: response.data, + responseTime: response.responseTime, timeline: response.timeline, - responseTime: response.headers.get('request-duration') }, ...eventData }); @@ -1187,6 +1188,8 @@ const registerNetworkIpc = (mainWindow) => { if (error?.response) { const { data, dataBuffer } = parseDataFromResponse(error.response); + error.response.responseTime = error.response.headers.get('request-duration'); + error.response.headers.delete('request-duration'); error.response.data = data; error.response.dataBuffer = dataBuffer; @@ -1199,8 +1202,8 @@ const registerNetworkIpc = (mainWindow) => { dataBuffer: dataBuffer.toString('base64'), size: Buffer.byteLength(dataBuffer), data: error.response.data, + responseTime: error.response.responseTime, timeline: error.response.timeline, - responseTime: error.response.headers.get('request-duration') }; // if we get a response from the server, we consider it as a success diff --git a/packages/bruno-js/src/bruno-response.js b/packages/bruno-js/src/bruno-response.js index dc78db285..1705d606f 100644 --- a/packages/bruno-js/src/bruno-response.js +++ b/packages/bruno-js/src/bruno-response.js @@ -52,6 +52,46 @@ class BrunoResponse { this.body = clonedData; } + // TODO: Refactor: dataBuffer size calculation should be handled in a shared utility so it can be passed and reused across the application + getSize() { + if (!this.res) { + return { header: 0, body: 0, total: 0 }; + } + + const { data, dataBuffer, headers } = this.res; + let bodySize = 0; + + // Use raw received bytes + if (Buffer.isBuffer(dataBuffer)) { + bodySize = dataBuffer.length; + } else { + // Use server-reported Content-Length + const contentLength = headers && (headers['content-length'] || headers['Content-Length']); + if (contentLength && !isNaN(contentLength)) { + bodySize = parseInt(contentLength, 10); + } else if (data != null) { + // Manual calculation + const raw = typeof data === 'string' ? data : JSON.stringify(data); + bodySize = Buffer.byteLength(raw); + } + } + + const headerLines = [ + `HTTP/1.1 ${this.res.status} ${this.res.statusText}`, + ...Object.entries(this.res.headers || {}).flatMap(([key, value]) => + Array.isArray(value) + ? value.map((v) => `${key}: ${v}`) + : [`${key}: ${value}`] + ), + '', + '' + ]; + const headerSize = Buffer.byteLength(headerLines.join('\r\n')); + + return { header: headerSize, body: bodySize, total: headerSize + bodySize }; + + } + getDataBuffer() { return this.res ? this.res.dataBuffer : null; } diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js index 6b9501876..50aca92ac 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js @@ -65,6 +65,12 @@ const addBrunoResponseShimToContext = (vm, res) => { vm.setProp(resFn, 'setBody', setBody); setBody.dispose(); + let getSize = vm.newFunction('getSize', function () { + return marshallToVm(res.getSize(), vm); + }); + vm.setProp(resFn, 'getSize', getSize); + getSize.dispose(); + vm.setProp(vm.global, 'res', resFn); resFn.dispose(); }; diff --git a/packages/bruno-tests/collection/scripting/api/res/getSize.bru b/packages/bruno-tests/collection/scripting/api/res/getSize.bru new file mode 100644 index 000000000..2d727ea07 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/getSize.bru @@ -0,0 +1,36 @@ +meta { + name: getSize + type: http + seq: 8 +} + +get { + url: https://www.httpfaker.org/api/random/json?size=1mb + body: none + auth: inherit +} + +params:query { + size: 1mb +} + +script:post-response { + console.log(res.getSize()) +} + +tests { + test("test body size", function() { + const bodySize = res.getSize().body; + expect(bodySize === 1048934).to.be.true; + }); + + test("test header size", function() { + const bodySize = res.getSize().header; + expect(bodySize === 305).to.be.true; + }); + + test("test total size", function() { + const sizes = res.getSize(); + expect(sizes.total).to.equal(sizes.header + sizes.body); + }); +}