Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/extension/ipc/network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions src/extension/ipc/network/prepare-request.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {};
prepareBody({ mode: 'json', json: '{}' }, headers);
expect(headers['content-type']).toBe('application/json');
});
});
16 changes: 13 additions & 3 deletions src/extension/ipc/network/prepare-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -84,13 +85,22 @@ const prepareBody = (body: BrunoRequest['body'], headers: Record<string, string>
}

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';
Expand Down
64 changes: 64 additions & 0 deletions src/extension/utils/strip-json-comments.ts
Original file line number Diff line number Diff line change
@@ -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;