From 73e828621fd0708791087a5378e8350991be1979 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:58:53 +0530 Subject: [PATCH] fix: enhance URL parameter parsing and interpolation logic (#5812) * fix: enhance URL parameter parsing and interpolation logic --- packages/bruno-app/src/utils/url/index.js | 72 +++++++++++++------ .../bruno-app/src/utils/url/index.spec.js | 50 +++++++++---- .../bruno-cli/src/runner/interpolate-vars.js | 36 ++++++++-- .../src/ipc/network/interpolate-vars.js | 36 ++++++++-- 4 files changed, 145 insertions(+), 49 deletions(-) diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index bff1ac895..87b37ee20 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -33,19 +33,35 @@ export const parsePathParams = (url) => { } // Enhanced: also match :param inside parentheses and/or quotes - const paramRegex = /[:](\w+)/g; const foundParams = new Set(); paths.forEach(segment => { + // traditional path parameters + if (segment.startsWith(':')) { + const name = segment.slice(1); + if (name && !foundParams.has(name)) { + foundParams.add(name); + } + return; + } + + // for OData-style parameters (parameters inside parentheses) + // Check if segment matches valid OData syntax: + // 1. EntitySet('key') or EntitySet(key) + // 2. EntitySet(Key1=value1,Key2=value2) + // 3. Function(param=value) + if (!/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(segment)) { + return; + } + + const paramRegex = /[:](\w+)/g; let match; while ((match = paramRegex.exec(segment))) { - if (match[1]) { - // Clean up: remove trailing quotes/parentheses if present - let name = match[1].replace(/[')"`]+$/, ''); - // Remove leading quotes/parentheses if present - name = name.replace(/^[('"`]+/, ''); - if (name && !foundParams.has(name)) { - foundParams.add(name); - } + if (!match[1]) continue; + + let name = match[1].replace(/[')"`]+$/, ''); + name = name.replace(/^[('"`]+/, ''); + if (name && !foundParams.has(name)) { + foundParams.add(name); } } }); @@ -84,27 +100,41 @@ export const interpolateUrl = ({ url, variables }) => { export const interpolateUrlPathParams = (url, params) => { const getInterpolatedBasePath = (pathname, params) => { - const regex = /[:](\w+)/g; return pathname .split('/') .map((segment) => { + // traditional path parameters + if (segment.startsWith(':')) { + const name = segment.slice(1); + const pathParam = params.find((p) => p?.name === name && p?.type === 'path'); + return pathParam ? pathParam.value : segment; + } - if (!segment.startsWith(':')) return segment; + // for OData-style parameters (parameters inside parentheses) + // Check if segment matches valid OData syntax: + // 1. EntitySet('key') or EntitySet(key) + // 2. EntitySet(Key1=value1,Key2=value2) + // 3. Function(param=value) + if (!/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(segment)) { + return segment; + } + const regex = /[:](\w+)/g; let match; + let result = segment; while ((match = regex.exec(segment))) { - if (match[1]) { - // Clean up: remove trailing quotes/parentheses if present - let name = match[1].replace(/[')"`]+$/, ''); - // Remove leading quotes/parentheses if present - name = name.replace(/^[('"`]+/, ''); - if (name) { - const pathParam = params.find(p => p?.name === name && p?.type === 'path'); - return pathParam ? pathParam.value : segment; - } + if (!match[1]) continue; + + let name = match[1].replace(/[')"`]+$/, ''); + name = name.replace(/^[('"`]+/, ''); + if (!name) continue; + + const pathParam = params.find((p) => p?.name === name && p?.type === 'path'); + if (pathParam) { + result = result.replace(':' + match[1], pathParam.value); } } - return segment; + return result; }) .join('/'); }; diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index ddab88a99..cff3ce2bb 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -80,13 +80,19 @@ describe('Url Utils - parsePathParams', () => { expect(params).toEqual([{ name: 'productId', value: '' }]); }); + it('should handle OData parameters with mixed quote types', () => { + const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Categories(":categoryId")'); + expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]); + }); + it('should parse OData entity key with parentheses only', () => { const params = parsePathParams('https://example.com/odata/Products(:productId)'); expect(params).toEqual([{ name: 'productId', value: '' }]); }); - it('should parse OData composite key with multiple parameters', () => { - const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=\':productId\')'); + it('should parse OData composite key with mixed parameter styles', () => { + // Test both positional and named parameter styles in the same key + const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=:productId)'); expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]); }); @@ -115,19 +121,24 @@ describe('Url Utils - parsePathParams', () => { expect(params).toEqual([{ name: 'product_id', value: '' }]); }); - it('should handle OData parameters with mixed quote types', () => { - const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Categories(":categoryId")'); - expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]); + it('should parse OData composite key with positional parameters', () => { + const params = parsePathParams('https://example.com/odata/Orders(:orderId,:productId)'); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]); }); - it('should handle OData parameters with nested parentheses', () => { - const params = parsePathParams('https://example.com/odata/Products((\':productId\'))'); - expect(params).toEqual([{ name: 'productId', value: '' }]); + it('should parse OData composite key with named parameters', () => { + const params = parsePathParams('https://example.com/odata/Orders(OrderId=:orderId,ProductId=:productId)'); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]); }); - it('should handle OData parameters with complex nested structures', () => { - const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')/Properties(\':propName\')'); - expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]); + it('should handle OData navigation properties', () => { + const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items'); + expect(params).toEqual([{ name: 'orderId', value: '' }]); + }); + + it('should handle OData function parameters', () => { + const params = parsePathParams('https://example.com/odata/Products/GetProductsByCategory(categoryId=:categoryId)'); + expect(params).toEqual([{ name: 'categoryId', value: '' }]); }); it('should handle OData parameters with query options in path', () => { @@ -145,9 +156,15 @@ describe('Url Utils - parsePathParams', () => { expect(params).toEqual([]); }); - it('should handle OData parameters with function calls in parentheses', () => { - const params = parsePathParams('https://example.com/odata/Products(GetId(\':productId\'))'); - expect(params).toEqual([{ name: 'productId', value: '' }]); + it('should NOT treat embedded colons as path parameters (regression fix)', () => { + // This test case reproduces the bug reported in issue #5805 + const params = parsePathParams('/start/1:2:AHLS-HASD/form'); + expect(params).toEqual([]); + }); + + it('should NOT treat embedded colons as path parameters in full URLs', () => { + const params = parsePathParams('https://example.com/start/1:2:AHLS-HASD/form'); + expect(params).toEqual([]); }); it('should handle OData parameters with escaped quotes', () => { @@ -169,6 +186,11 @@ describe('Url Utils - parsePathParams', () => { const params = parsePathParams('https://example.com/odata/Products(\'123e4567-e89b-12d3-a456-426614174000\')'); expect(params).toEqual([]); }); + + it('should handle OData with query parameters for variable interpolation', () => { + const params = parsePathParams('https://example.com/odata/Products?$filter=Category eq \'{{category}}\'&$orderby={{sortField}}'); + expect(params).toEqual([]); + }); }); describe('Url Utils - splitOnFirst', () => { diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 7e19265b1..caa6293cf 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -116,22 +116,44 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc throw { message: 'Invalid URL format', originalError: e.message }; } - const paramRegex = /[:](\w+)/g; const interpolatedUrlPath = url.pathname .split('/') .filter((path) => path !== '') .map((path) => { - const matches = path.match(paramRegex); - if (matches) { - const paramName = matches[0].slice(1); // Remove the : prefix + // traditional path parameters + if (path.startsWith(':')) { + const paramName = path.slice(1); const existingPathParam = request.pathParams.find(param => param.name === paramName); if (!existingPathParam) { return '/' + path; } - return '/' + path.replace(':' + paramName, existingPathParam.value); - } else { - return '/' + path; + return '/' + existingPathParam.value; } + + // for OData-style parameters (parameters inside parentheses) + // Check if path matches valid OData syntax: + // 1. EntitySet('key') or EntitySet(key) + // 2. EntitySet(Key1=value1,Key2=value2) + // 3. Function(param=value) + if (/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(path)) { + const paramRegex = /[:](\w+)/g; + let match; + let result = path; + while ((match = paramRegex.exec(path))) { + if (match[1]) { + let name = match[1].replace(/[')"`]+$/, ''); + name = name.replace(/^[('"`]+/, ''); + if (name) { + const existingPathParam = request.pathParams.find((param) => param.name === name); + if (existingPathParam) { + result = result.replace(':' + match[1], existingPathParam.value); + } + } + } + } + return '/' + result; + } + return '/' + path; }) .join(''); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 569b06f7d..92decafec 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -141,22 +141,44 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc throw { message: 'Invalid URL format', originalError: e.message }; } - const paramRegex = /[:](\w+)/g; const urlPathnameInterpolatedWithPathParams = url.pathname .split('/') .filter((path) => path !== '') .map((path) => { - const matches = path.match(paramRegex); - if (matches) { - const paramName = matches[0].slice(1); // Remove the : prefix + // traditional path parameters + if (path.startsWith(':')) { + const paramName = path.slice(1); const existingPathParam = request.pathParams.find(param => param.name === paramName); if (!existingPathParam) { return '/' + path; } - return '/' + path.replace(':' + paramName, existingPathParam.value); - } else { - return '/' + path; + return '/' + existingPathParam.value; } + + // for OData-style parameters (parameters inside parentheses) + // Check if path matches valid OData syntax: + // 1. EntitySet('key') or EntitySet(key) + // 2. EntitySet(Key1=value1,Key2=value2) + // 3. Function(param=value) + if (/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(path)) { + const paramRegex = /[:](\w+)/g; + let match; + let result = path; + while ((match = paramRegex.exec(path))) { + if (match[1]) { + let name = match[1].replace(/[')"`]+$/, ''); + name = name.replace(/^[('"`]+/, ''); + if (name) { + const existingPathParam = request.pathParams.find((param) => param.name === name); + if (existingPathParam) { + result = result.replace(':' + match[1], existingPathParam.value); + } + } + } + } + return '/' + result; + } + return '/' + path; }) .join('');