diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 92459f61d..5bef312e8 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -73,6 +73,12 @@ const hasStreamHeaders = (headers) => { return headerSplit.indexOf('text/event-stream') > -1; }; +const buildResponseBodyFromStreamChunks = (sseChunks, headers, disableParsingResponseJson) => { + const dataBuffer = Buffer.concat(sseChunks); + const { data } = parseDataFromResponse({ data: dataBuffer, headers }, disableParsingResponseJson); + return { data, dataBuffer }; +}; + const promisifyStream = async (stream, abortController, closeOnFirst) => { const chunks = []; @@ -1014,6 +1020,7 @@ const registerNetworkIpc = (mainWindow) => { } let response, responseTime, axiosDataStream; + const sseChunks = []; try { /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); @@ -1243,7 +1250,22 @@ const registerNetworkIpc = (mainWindow) => { } }; if (isResponseStream) { - axiosDataStream.on('close', () => runPostScripts().then()); + axiosDataStream.on('close', () => { + try { + const { data, dataBuffer } = buildResponseBodyFromStreamChunks( + sseChunks, + response.headers, + request.__brunoDisableParsingResponseJson + ); + response.data = data; + response.dataBuffer = dataBuffer; + } catch (error) { + console.error('Error rebuilding response body from SSE chunks:', error); + } + runPostScripts().catch((error) => { + console.error('Error running post-response scripts for SSE stream:', error); + }); + }); } else { await runPostScripts(); } @@ -1254,6 +1276,7 @@ const registerNetworkIpc = (mainWindow) => { headers: response.headers, data: response.data, stream: isResponseStream ? axiosDataStream : null, + sseChunks: isResponseStream ? sseChunks : null, cancelTokenUid: cancelTokenUid, dataBuffer: response.dataBuffer.toString('base64'), size: Buffer.byteLength(response.dataBuffer), @@ -1332,6 +1355,8 @@ const registerNetworkIpc = (mainWindow) => { response.stream = { running: response.status >= 200 && response.status < 300 }; stream.on('data', (newData) => { + // Collect the raw chunk so runRequest can rebuild the full body on stream close. + response.sseChunks?.push(newData); seq += 1; const parsed = parseDataFromResponse({ data: newData, headers: {} }); @@ -2248,3 +2273,4 @@ module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler; module.exports.executeRequestOnFailHandler = executeRequestOnFailHandler; +module.exports.buildResponseBodyFromStreamChunks = buildResponseBodyFromStreamChunks; diff --git a/packages/bruno-tests/collection/sse/sse finite stream.bru b/packages/bruno-tests/collection/sse/sse finite stream.bru new file mode 100644 index 000000000..72348b88f --- /dev/null +++ b/packages/bruno-tests/collection/sse/sse finite stream.bru @@ -0,0 +1,35 @@ +meta { + name: sse finite stream + type: http + seq: 1 +} + +get { + url: {{localhost}}/api/sse/finite + body: none + auth: none +} + +script:post-response { + const body = res.getBody(); + bru.setVar("sseBody", typeof body === "string" ? body : JSON.stringify(body)); +} + +tests { + test("status is 200", function() { + expect(res.status).to.equal(200); + }); + + test("content-type is text/event-stream", function() { + expect(res.headers["content-type"]).to.include("text/event-stream"); + }); + + test("res.getBody() contains all 3 SSE events in order", function() { + const body = res.getBody(); + expect(body).to.include("data: Hello"); + expect(body).to.include("data: from"); + expect(body).to.include("data: SSE"); + expect(body.indexOf("data: Hello")).to.be.lessThan(body.indexOf("data: from")); + expect(body.indexOf("data: from")).to.be.lessThan(body.indexOf("data: SSE")); + }); +} diff --git a/packages/bruno-tests/src/sse/index.js b/packages/bruno-tests/src/sse/index.js index 0cd4cfb49..6b8b9ad44 100644 --- a/packages/bruno-tests/src/sse/index.js +++ b/packages/bruno-tests/src/sse/index.js @@ -55,4 +55,27 @@ router.post('/reset', (req, res) => { res.json({ message: 'Reset complete', activeConnections: 0 }); }); +// GET /api/sse/finite - Sends one event per message then closes. +router.get('/finite', (req, res) => { + const messages = ['Hello', 'from', 'SSE']; + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + + let index = 0; + const interval = setInterval(() => { + res.write(`data: ${messages[index]}\n\n`); + index += 1; + if (index >= messages.length) { + clearInterval(interval); + res.end(); + } + }, 50); + + req.on('close', () => clearInterval(interval)); +}); + module.exports = router;