From f70731bbdcc08672f4cd3f085198a930f4722c88 Mon Sep 17 00:00:00 2001 From: CauchYoung <2024302072042@whu.edu.cn> Date: Sat, 23 May 2026 02:15:18 +0800 Subject: [PATCH] fix: preserve symbol keys in merged request data (#10812) * fix: preserve symbol keys in merged request data * fix: address symbol merge review feedback * fix: align symbol merge with v1.x own-prop guard * fix: avoid merge conflict on target key handling * feat: collapsed the duplicated typeof key === symbol branch in assignValue * chore: added should honor skipUndefined for symbol keys to lock in skipUndefined semantics * chore: added should pass symbol keys to transformRequest through axios.create covering the instance * fix(utils): skip symbol scan for arrays --------- Co-authored-by: laplace young Co-authored-by: Jay --- PRE_RELEASE_CHANGELOG.md | 4 ++ lib/utils.js | 25 +++++++++++- tests/unit/api.test.js | 61 ++++++++++++++++++++++++++++ tests/unit/utils/merge.test.js | 74 ++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 2 deletions(-) diff --git a/PRE_RELEASE_CHANGELOG.md b/PRE_RELEASE_CHANGELOG.md index 33908c59..3af9f649 100644 --- a/PRE_RELEASE_CHANGELOG.md +++ b/PRE_RELEASE_CHANGELOG.md @@ -6,6 +6,10 @@ - **HTTP Adapter - Zstandard:** Added automatic zstd decompression on Node.js versions that support it. `zstd` is only advertised in the default `Accept-Encoding` header when `transitional.advertiseZstdAcceptEncoding: true` is set. (**#6792**) +## Bug Fixes + +- **Request Data:** Preserve enumerable symbol keys when merging plain request data before `transformRequest`. (**#6392**) + ## Release Documentation TODO - Update `README.md` request config docs for `transitional.advertiseZstdAcceptEncoding` and zstd decompression support. diff --git a/lib/utils.js b/lib/utils.js index a869a938..5cd8f10c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -412,7 +412,9 @@ function merge(...objs) { return; } - const targetKey = (caseless && findKey(result, key)) || key; + // findKey lowercases the key, so caseless lookup only applies to strings — + // symbol keys are identity-matched. + const targetKey = (caseless && typeof key === 'string' && findKey(result, key)) || key; // Read via own-prop only — a bare `result[targetKey]` walks the prototype // chain, so a polluted Object.prototype value could surface here and get // copied into the merged result. @@ -429,7 +431,24 @@ function merge(...objs) { }; for (let i = 0, l = objs.length; i < l; i++) { - objs[i] && forEach(objs[i], assignValue); + const source = objs[i]; + if (!source || isBuffer(source)) { + continue; + } + + forEach(source, assignValue); + + if (typeof source !== 'object' || isArray(source)) { + continue; + } + + const symbols = Object.getOwnPropertySymbols(source); + for (let j = 0; j < symbols.length; j++) { + const symbol = symbols[j]; + if (propertyIsEnumerable.call(source, symbol)) { + assignValue(source[symbol], symbol); + } + } } return result; } @@ -658,6 +677,8 @@ const hasOwnProperty = ( hasOwnProperty.call(obj, prop) )(Object.prototype); +const { propertyIsEnumerable } = Object.prototype; + /** * Determine if a value is a RegExp object * diff --git a/tests/unit/api.test.js b/tests/unit/api.test.js index bcfd2ec4..9dc3009d 100644 --- a/tests/unit/api.test.js +++ b/tests/unit/api.test.js @@ -80,6 +80,36 @@ describe('static api', () => { it('should have getAdapter properties', () => { assert.strictEqual(typeof axios.getAdapter, 'function'); }); + + it('should pass symbol keys to transformRequest', async () => { + const symbolKey = Symbol('example'); + let transformedData; + + await axios.post( + '/test', + { + [symbolKey]: 'value', + stringKey: 'value', + }, + { + transformRequest(data) { + transformedData = data; + return ''; + }, + adapter: (config) => + Promise.resolve({ + data: null, + status: 200, + statusText: 'OK', + headers: {}, + config, + request: {}, + }), + } + ); + + assert.strictEqual(transformedData[symbolKey], 'value'); + }); }); describe('instance api', () => { @@ -101,4 +131,35 @@ describe('instance api', () => { assert.strictEqual(typeof instance.interceptors.request, 'object'); assert.strictEqual(typeof instance.interceptors.response, 'object'); }); + + it('should pass symbol keys to transformRequest through axios.create', async () => { + const symbolKey = Symbol('example'); + let transformedData; + + const client = axios.create({ + transformRequest: [ + (data) => { + transformedData = data; + return ''; + }, + ], + adapter: (config) => + Promise.resolve({ + data: null, + status: 200, + statusText: 'OK', + headers: {}, + config, + request: {}, + }), + }); + + await client.post('/test', { + [symbolKey]: 'value', + stringKey: 'value', + }); + + assert.strictEqual(transformedData[symbolKey], 'value'); + assert.strictEqual(transformedData.stringKey, 'value'); + }); }); diff --git a/tests/unit/utils/merge.test.js b/tests/unit/utils/merge.test.js index 76f2106a..48a6df72 100644 --- a/tests/unit/utils/merge.test.js +++ b/tests/unit/utils/merge.test.js @@ -94,4 +94,78 @@ describe('utils::merge', () => { x: 2, }); }); + + it('should merge enumerable symbol keys', () => { + const key = Symbol('key'); + const nestedKey = Symbol('nested'); + const first = { [key]: { first: true } }; + const second = { + [key]: { second: true }, + nested: { + [nestedKey]: 'value', + }, + }; + const merged = merge(first, second); + + expect(merged[key]).toEqual({ first: true, second: true }); + expect(merged[key]).not.toBe(first[key]); + expect(merged.nested[nestedKey]).toBe('value'); + expect(merged.nested).not.toBe(second.nested); + }); + + it('should skip non-enumerable symbol keys', () => { + const key = Symbol('key'); + const source = {}; + + Object.defineProperty(source, key, { + value: 'hidden', + enumerable: false, + }); + + const merged = merge(source); + + expect(merged[key]).toBeUndefined(); + expect(Object.getOwnPropertySymbols(merged)).toEqual([]); + }); + + it('should support caseless string keys with symbol keys', () => { + const key = Symbol('key'); + const merged = merge.call( + { caseless: true }, + { x: 1, [key]: 'first' }, + { X: 2, [key]: 'second' } + ); + + expect(merged.x).toBe(2); + expect(merged.X).toBeUndefined(); + expect(merged[key]).toBe('second'); + }); + + it('should ignore symbol keys on buffers', () => { + const key = Symbol('key'); + const buffer = Buffer.from('value'); + buffer[key] = 'symbol value'; + + const merged = merge({ x: 1 }, buffer); + + expect(merged).toEqual({ x: 1 }); + }); + + it('should ignore symbol keys on arrays', () => { + const key = Symbol('key'); + const array = ['value']; + array[key] = 'symbol value'; + + const merged = merge({ x: 1 }, array); + + expect(merged).toEqual({ 0: 'value', x: 1 }); + expect(merged[key]).toBeUndefined(); + }); + + it('should honor skipUndefined for symbol keys', () => { + const key = Symbol('key'); + const merged = merge.call({ skipUndefined: true }, { [key]: 'first' }, { [key]: undefined }); + + expect(merged[key]).toBe('first'); + }); });