mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
fix: graphQL variables interpolation consistency (UI and CLI) (#7049)
* feat: enhance GraphQL request handling with variable interpolation
This commit is contained in:
@@ -67,40 +67,47 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
});
|
||||
|
||||
const contentType = getContentType(request.headers);
|
||||
const isGraphqlRequest = request.mode === 'graphql';
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
// Skip interpolation if data is a Buffer (e.g., gzip-compressed data)
|
||||
if (typeof request.data === 'object' && !Buffer.isBuffer(request.data)) {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = _interpolate(parsed, { escapeJSONStrings: true });
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {}
|
||||
}
|
||||
// GraphQL: interpolate query and variables in place. We do not stringify the whole body and interpolate that, because variables is a JSON string. Full-body stringify would nest it and double-escape any {{var}} inside.
|
||||
if (isGraphqlRequest && request.data && typeof request.data === 'object') {
|
||||
request.data.query = _interpolate(request.data.query, { escapeJSONStrings: true });
|
||||
request.data.variables = _interpolate(request.data.variables, { escapeJSONStrings: true });
|
||||
}
|
||||
|
||||
if (typeof request.data === 'string') {
|
||||
if (request?.data?.length) {
|
||||
request.data = _interpolate(request.data, { escapeJSONStrings: true });
|
||||
// Skip body interpolation for GraphQL requests.
|
||||
if (!isGraphqlRequest) {
|
||||
if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {
|
||||
if (typeof request.data === 'string') {
|
||||
if (request?.data?.length) {
|
||||
request.data = _interpolate(request.data, { escapeJSONStrings: true });
|
||||
}
|
||||
} else if (typeof request.data === 'object') {
|
||||
try {
|
||||
let parsed = JSON.stringify(request.data);
|
||||
parsed = _interpolate(parsed, { escapeJSONStrings: true });
|
||||
request.data = JSON.parse(parsed);
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
if (request.data && Array.isArray(request.data)) {
|
||||
request.data = request.data.map((d) => ({
|
||||
...d,
|
||||
value: _interpolate(d?.value)
|
||||
}));
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
try {
|
||||
request.data = request?.data?.map((d) => ({
|
||||
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||
if (request.data && Array.isArray(request.data)) {
|
||||
request.data = request.data.map((d) => ({
|
||||
...d,
|
||||
value: _interpolate(d?.value)
|
||||
}));
|
||||
} catch (err) {}
|
||||
}
|
||||
} else if (contentType === 'multipart/form-data') {
|
||||
if (Array.isArray(request?.data) && !isFormData(request.data)) {
|
||||
try {
|
||||
request.data = request?.data?.map((d) => ({
|
||||
...d,
|
||||
value: _interpolate(d?.value)
|
||||
}));
|
||||
} catch (err) {}
|
||||
}
|
||||
} else {
|
||||
request.data = _interpolate(request.data);
|
||||
}
|
||||
} else {
|
||||
request.data = _interpolate(request.data);
|
||||
}
|
||||
|
||||
each(request?.pathParams, (param) => {
|
||||
|
||||
@@ -45,7 +45,8 @@ const prepareRequest = async (item = {}, collection = {}) => {
|
||||
tags: item.tags || [],
|
||||
pathParams: request.params?.filter((param) => param.type === 'path'),
|
||||
settings: item.settings,
|
||||
responseType: 'arraybuffer'
|
||||
responseType: 'arraybuffer',
|
||||
mode: request.body?.mode
|
||||
};
|
||||
|
||||
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
||||
@@ -371,7 +372,8 @@ const prepareRequest = async (item = {}, collection = {}) => {
|
||||
if (request.body.mode === 'graphql') {
|
||||
const graphqlQuery = {
|
||||
query: get(request, 'body.graphql.query'),
|
||||
variables: JSON.parse(decomment(get(request, 'body.graphql.variables') || '{}'))
|
||||
// Parse variables only after interpolation (github.com/usebruno/bruno/issues/884)
|
||||
variables: decomment(get(request, 'body.graphql.variables') || '{}')
|
||||
};
|
||||
if (!contentTypeDefined) {
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
|
||||
@@ -308,6 +308,16 @@ const runSingleRequest = async function (
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, runtimeVariables, processEnvVars);
|
||||
|
||||
// if this is a graphql request, parse the variables, only after interpolation
|
||||
// https://github.com/usebruno/bruno/issues/884
|
||||
if (request.mode === 'graphql' && typeof request.data?.variables === 'string') {
|
||||
try {
|
||||
request.data.variables = JSON.parse(request.data.variables);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse GraphQL variables: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.settings?.encodeUrl) {
|
||||
request.url = encodeUrl(request.url);
|
||||
}
|
||||
|
||||
@@ -600,4 +600,50 @@ describe('prepare-request: prepareRequest', () => {
|
||||
expect(readFileSyncSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GraphQL request', () => {
|
||||
it('keeps variables as string for interpolation', async () => {
|
||||
const item = {
|
||||
request: {
|
||||
method: 'POST',
|
||||
headers: [],
|
||||
params: [],
|
||||
url: 'https://example.com',
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: 'query { x }',
|
||||
variables: '{"apiPermissions": {{permissionsJSON}}}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = await prepareRequest(item);
|
||||
expect(result.mode).toBe('graphql');
|
||||
expect(result.data).toMatchObject({ query: 'query { x }' });
|
||||
expect(typeof result.data.variables).toBe('string');
|
||||
expect(result.data.variables).toBe('{"apiPermissions": {{permissionsJSON}}}');
|
||||
});
|
||||
|
||||
it('defaults variables to "{}" when missing', async () => {
|
||||
const item = {
|
||||
request: {
|
||||
method: 'POST',
|
||||
headers: [],
|
||||
params: [],
|
||||
url: 'https://example.com',
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: 'query { x }',
|
||||
variables: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = await prepareRequest(item);
|
||||
expect(typeof result.data.variables).toBe('string');
|
||||
expect(result.data.variables).toBe('{}');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,8 +540,12 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// if this is a graphql request, parse the variables, only after interpolation
|
||||
// https://github.com/usebruno/bruno/issues/884
|
||||
if (request.mode === 'graphql') {
|
||||
request.data.variables = JSON.parse(request.data.variables);
|
||||
if (request.mode === 'graphql' && typeof request.data?.variables === 'string') {
|
||||
try {
|
||||
request.data.variables = JSON.parse(request.data.variables);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse GraphQL variables: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// stringify the request url encoded params
|
||||
|
||||
@@ -74,6 +74,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
});
|
||||
|
||||
const contentType = getContentType(request.headers);
|
||||
const isGraphqlRequest = request.mode === 'graphql';
|
||||
|
||||
if (isGrpcRequest) {
|
||||
const jsonDoc = JSON.stringify(request.body);
|
||||
@@ -103,7 +104,13 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof contentType === 'string') {
|
||||
// GraphQL: interpolate query and variables in place. We do not stringify the whole body and interpolate that, because variables is a JSON string. Full-body stringify would nest it and double-escape any {{var}} inside.
|
||||
if (isGraphqlRequest && request.data && typeof request.data === 'object') {
|
||||
request.data.query = _interpolate(request.data.query, { escapeJSONStrings: true });
|
||||
request.data.variables = _interpolate(request.data.variables, { escapeJSONStrings: true });
|
||||
}
|
||||
|
||||
if (typeof contentType === 'string' && !isGraphqlRequest) {
|
||||
/*
|
||||
We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode.
|
||||
Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated.
|
||||
|
||||
@@ -39,4 +39,29 @@ describe('prepare-request: prepareRequest', () => {
|
||||
expect(result.headers['content-type']).toEqual('application/json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GraphQL request', () => {
|
||||
it('keeps variables as string for interpolation', async () => {
|
||||
const item = {
|
||||
request: {
|
||||
method: 'POST',
|
||||
headers: [],
|
||||
params: [],
|
||||
url: 'https://example.com',
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: 'query { x }',
|
||||
variables: '{"apiPermissions": {{permissionsJSON}}}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = await prepareRequest(item);
|
||||
expect(result.mode).toBe('graphql');
|
||||
expect(result.data).toMatchObject({ query: 'query { x }' });
|
||||
expect(typeof result.data.variables).toBe('string');
|
||||
expect(result.data.variables).toBe('{"apiPermissions": {{permissionsJSON}}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
meta {
|
||||
name: variables interpolation
|
||||
type: graphql
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: graphql
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:graphql {
|
||||
query { __typename }
|
||||
}
|
||||
|
||||
body:graphql:vars {
|
||||
{
|
||||
"my_json": "{{my_json}}"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const testData = {
|
||||
a: [1,2,3],
|
||||
b: {
|
||||
c: "test",
|
||||
d: "another value"
|
||||
}
|
||||
};
|
||||
|
||||
// Single escaping
|
||||
let cv = JSON.stringify(testData).replace(/"/g, '\\"');
|
||||
|
||||
bru.setVar("my_json", cv)
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
bru.deleteVar("my_json")
|
||||
}
|
||||
|
||||
tests {
|
||||
test("GraphQL variables with nested object and array are interpolated then sent as parsed object", function() {
|
||||
const body = res.getBody();
|
||||
expect(body).to.have.property("variables");
|
||||
expect(body.variables).to.be.an("object");
|
||||
expect(body.variables).to.have.property("my_json");
|
||||
expect(body.variables.my_json).to.eql("{\"a\":[1,2,3],\"b\":{\"c\":\"test\",\"d\":\"another value\"}}");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user