From 859debe8eb3d6e5cdc2efa6558bf9b9261f14aae Mon Sep 17 00:00:00 2001 From: Alan Mendoza Date: Mon, 24 Aug 2026 11:17:42 -0300 Subject: [PATCH 1/4] feat: add a string-aware json comment stripper --- src/extension/utils/strip-json-comments.ts | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/extension/utils/strip-json-comments.ts diff --git a/src/extension/utils/strip-json-comments.ts b/src/extension/utils/strip-json-comments.ts new file mode 100644 index 0000000..1783f41 --- /dev/null +++ b/src/extension/utils/strip-json-comments.ts @@ -0,0 +1,64 @@ +/** + * Removes `//` line comments and block comments from a JSON body, leaving string literals + * untouched. + * + * The desktop app and the CLI run the request body through `decomment` before sending it + * (see `bruno-electron/src/ipc/network/prepare-request.js`), so collections are commonly + * authored with comments. This keeps the extension in line without pulling in a JavaScript + * parser: a JSON body has no regular expressions or template literals, so a string-aware + * scan is enough to tell a comment from a `//` that happens to live inside a value. + */ +export const stripJsonComments = (text: string): string => { + let result = ''; + let index = 0; + let insideString = false; + let escaped = false; + + while (index < text.length) { + const char = text[index]; + + if (insideString) { + result += char; + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === '"') { + insideString = false; + } + index++; + continue; + } + + if (char === '"') { + insideString = true; + result += char; + index++; + continue; + } + + // The newline is left in place: dropping it would join the next line to this one. + if (char === '/' && text[index + 1] === '/') { + while (index < text.length && text[index] !== '\n') { + index++; + } + continue; + } + + if (char === '/' && text[index + 1] === '*') { + index += 2; + while (index < text.length && !(text[index] === '*' && text[index + 1] === '/')) { + index++; + } + index += 2; + continue; + } + + result += char; + index++; + } + + return result; +}; + +export default stripJsonComments; From 2569c7f0ba7036b1f3c246d0fc50ba55d4fe973d Mon Sep 17 00:00:00 2001 From: Alan Mendoza Date: Mon, 24 Aug 2026 11:17:44 -0300 Subject: [PATCH 2/4] fix: strip comments from the json body before sending it --- src/extension/ipc/network/prepare-request.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/extension/ipc/network/prepare-request.ts b/src/extension/ipc/network/prepare-request.ts index 9f6b9ed..6d092c5 100644 --- a/src/extension/ipc/network/prepare-request.ts +++ b/src/extension/ipc/network/prepare-request.ts @@ -2,6 +2,7 @@ import type { AxiosRequestConfig } from 'axios'; import { get, filter } from 'lodash'; import { utils as brunoUtilsRaw } from '@usebruno/common'; +import { stripJsonComments } from '../../utils/strip-json-comments'; // Type assertion for @usebruno/common utils (no type definitions available) const brunoUtils = brunoUtilsRaw as { @@ -84,13 +85,22 @@ const prepareBody = (body: BrunoRequest['body'], headers: Record } switch (body.mode) { - case 'json': + case 'json': { headers['content-type'] = headers['content-type'] || 'application/json'; + if (!body.json) { + return undefined; + } + // Comments are stripped the way the desktop app and the CLI do it. Without this a + // commented body fails to parse, and axios ends up sending the text as a JSON string. + const json = stripJsonComments(body.json); try { - return body.json ? JSON.parse(body.json) : undefined; + return JSON.parse(json); } catch { - return body.json; + // Still not valid JSON on its own: unquoted variables are only resolved later, by + // interpolation. Hand over the comment-free text so it can parse after that. + return json; } + } case 'text': headers['content-type'] = headers['content-type'] || 'text/plain'; From ad998957d43824b7146909a89dd742f636922776 Mon Sep 17 00:00:00 2001 From: Alan Mendoza Date: Mon, 24 Aug 2026 11:17:45 -0300 Subject: [PATCH 3/4] test: cover json bodies with comments --- .../ipc/network/prepare-request.spec.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/extension/ipc/network/prepare-request.spec.ts diff --git a/src/extension/ipc/network/prepare-request.spec.ts b/src/extension/ipc/network/prepare-request.spec.ts new file mode 100644 index 0000000..58b3e4a --- /dev/null +++ b/src/extension/ipc/network/prepare-request.spec.ts @@ -0,0 +1,47 @@ +import { describe, test, expect } from 'vitest'; +import { prepareBody } from './prepare-request'; + +const jsonBody = (json: string) => prepareBody({ mode: 'json', json }, {}); + +describe('prepareBody strips comments from a json body', () => { + test('line comment', () => { + expect(jsonBody('{\n "a": 1 // note\n}')).toEqual({ a: 1 }); + }); + + test('comment on its own line', () => { + expect(jsonBody('{\n // note\n "a": 1\n}')).toEqual({ a: 1 }); + }); + + test('block comment', () => { + expect(jsonBody('{\n /* note */ "a": 1\n}')).toEqual({ a: 1 }); + }); + + test('a double slash inside a value is not a comment', () => { + expect(jsonBody('{\n "url": "http://example.com//path" // note\n}')).toEqual({ + url: 'http://example.com//path' + }); + }); + + test('escaped quotes inside a value survive', () => { + expect(jsonBody('{\n "quote": "he said \\"hi \\"" // note\n}')).toEqual({ + quote: 'he said "hi "' + }); + }); + + test('a body that is still invalid falls back to the comment-free text', () => { + // Unquoted variables only become valid JSON after interpolation, which runs later. + const result = jsonBody('{\n "id": {{id}} // note\n}'); + expect(typeof result).toBe('string'); + expect(result).not.toContain('//'); + }); + + test('an empty body sends nothing', () => { + expect(jsonBody('')).toBeUndefined(); + }); + + test('the content type is still defaulted', () => { + const headers: Record = {}; + prepareBody({ mode: 'json', json: '{}' }, headers); + expect(headers['content-type']).toBe('application/json'); + }); +}); From 8aef0b3962518e81a0519a2998c88900dee22bcd Mon Sep 17 00:00:00 2001 From: Alan Mendoza Date: Mon, 24 Aug 2026 11:40:45 -0300 Subject: [PATCH 4/4] fix: strip json body comments in getRequestData too --- src/extension/ipc/network/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/extension/ipc/network/index.ts b/src/extension/ipc/network/index.ts index 0b9972f..6c3744a 100644 --- a/src/extension/ipc/network/index.ts +++ b/src/extension/ipc/network/index.ts @@ -10,6 +10,7 @@ import { getCookieStringForUrl, saveCookies } from '../../utils/cookies'; import { createFormData, formatMultipartData } from '../../utils/form-data'; import { readFileBody, getSelectedFileBodyEntry, DEFAULT_FILE_BODY_CONTENT_TYPE } from '../../utils/file-body'; import { safeStringifyJSON } from '../../utils/common'; +import { stripJsonComments } from '../../utils/strip-json-comments'; import { getPreferences, preferencesUtil } from '../../store/preferences'; import { getProcessEnvVars } from '../../store/process-env'; import { getCertsAndProxyConfig } from './cert-utils'; @@ -618,7 +619,9 @@ const getRequestData = (body: BrunoRequest['body']): unknown => { switch (body.mode) { case 'json': - return body.json || undefined; + // Stripped the way the desktop app and the CLI do it. A commented body is not valid + // JSON, and axios ends up sending the whole text as a JSON string instead of an object. + return body.json ? stripJsonComments(body.json) : undefined; case 'text': return body.text || undefined;