From 684d3933855d42d6bb53c1635d3db8c230263625 Mon Sep 17 00:00:00 2001 From: Haozhenyu123 <2274273481@qq.com> Date: Tue, 9 Jun 2026 18:44:07 +0800 Subject: [PATCH] fix: encode request query parameters --- frontend/package.json | 1 + frontend/src/request/buildQueryString.js | 13 +++++++ frontend/src/request/buildQueryString.test.js | 37 +++++++++++++++++++ frontend/src/request/request.js | 29 +++------------ 4 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 frontend/src/request/buildQueryString.js create mode 100644 frontend/src/request/buildQueryString.test.js diff --git a/frontend/package.json b/frontend/package.json index 23e979115b..b71e2425cd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "test": "node --test", "preview": "vite preview", "dev:remote": "cross-env VITE_DEV_REMOTE=remote npm run dev" }, diff --git a/frontend/src/request/buildQueryString.js b/frontend/src/request/buildQueryString.js new file mode 100644 index 0000000000..9241c1e26b --- /dev/null +++ b/frontend/src/request/buildQueryString.js @@ -0,0 +1,13 @@ +export function buildQueryString(options = {}) { + const searchParams = new URLSearchParams(); + + Object.entries(options).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.set(key, value); + } + }); + + const queryString = searchParams.toString(); + + return queryString ? `?${queryString}` : ''; +} diff --git a/frontend/src/request/buildQueryString.test.js b/frontend/src/request/buildQueryString.test.js new file mode 100644 index 0000000000..5fe947f80f --- /dev/null +++ b/frontend/src/request/buildQueryString.test.js @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { buildQueryString } from './buildQueryString.js'; + +test('returns an empty string when no options are provided', () => { + assert.equal(buildQueryString(), ''); +}); + +test('encodes reserved characters and preserves their values', () => { + const queryString = buildQueryString({ + q: 'ACME & Sons + Partners #1', + fields: 'name,email', + }); + const searchParams = new URLSearchParams(queryString); + + assert.equal(searchParams.get('q'), 'ACME & Sons + Partners #1'); + assert.equal(searchParams.get('fields'), 'name,email'); + assert.match(queryString, /%26/); + assert.match(queryString, /%2B/); + assert.match(queryString, /%23/); +}); + +test('preserves falsy values and omits missing values', () => { + const queryString = buildQueryString({ + page: 0, + enabled: false, + filter: undefined, + equal: null, + }); + const searchParams = new URLSearchParams(queryString); + + assert.equal(searchParams.get('page'), '0'); + assert.equal(searchParams.get('enabled'), 'false'); + assert.equal(searchParams.has('filter'), false); + assert.equal(searchParams.has('equal'), false); +}); diff --git a/frontend/src/request/request.js b/frontend/src/request/request.js index 2679ade733..a1b448e528 100644 --- a/frontend/src/request/request.js +++ b/frontend/src/request/request.js @@ -4,6 +4,7 @@ import { API_BASE_URL } from '@/config/serverApiConfig'; import errorHandler from './errorHandler'; import successHandler from './successHandler'; import storePersist from '@/redux/storePersist'; +import { buildQueryString } from './buildQueryString'; function findKeyByPrefix(object, prefix) { for (var property in object) { @@ -116,9 +117,7 @@ const request = { filter: async ({ entity, options = {} }) => { try { includeToken(); - let filter = options.filter ? 'filter=' + options.filter : ''; - let equal = options.equal ? '&equal=' + options.equal : ''; - let query = `?${filter}${equal}`; + const query = buildQueryString(options); const response = await axios.get(entity + '/filter' + query); successHandler(response, { @@ -134,11 +133,7 @@ const request = { search: async ({ entity, options = {} }) => { try { includeToken(); - let query = '?'; - for (var key in options) { - query += key + '=' + options[key] + '&'; - } - query = query.slice(0, -1); + const query = buildQueryString(options); // headersInstance.cancelToken = source.token; const response = await axios.get(entity + '/search' + query); @@ -155,11 +150,7 @@ const request = { list: async ({ entity, options = {} }) => { try { includeToken(); - let query = '?'; - for (var key in options) { - query += key + '=' + options[key] + '&'; - } - query = query.slice(0, -1); + const query = buildQueryString(options); const response = await axios.get(entity + '/list' + query); @@ -175,11 +166,7 @@ const request = { listAll: async ({ entity, options = {} }) => { try { includeToken(); - let query = '?'; - for (var key in options) { - query += key + '=' + options[key] + '&'; - } - query = query.slice(0, -1); + const query = buildQueryString(options); const response = await axios.get(entity + '/listAll' + query); @@ -253,11 +240,7 @@ const request = { summary: async ({ entity, options = {} }) => { try { includeToken(); - let query = '?'; - for (var key in options) { - query += key + '=' + options[key] + '&'; - } - query = query.slice(0, -1); + const query = buildQueryString(options); const response = await axios.get(entity + '/summary' + query); successHandler(response, {