2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

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 <yangqk12@whu.edu.cn>
Co-authored-by: Jay <jasonsaayman@gmail.com>
This commit is contained in:
CauchYoung
2026-05-23 02:15:18 +08:00
committed by GitHub
parent 315ec44489
commit f70731bbdc
4 changed files with 162 additions and 2 deletions
+4
View File
@@ -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.
+23 -2
View File
@@ -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
*
+61
View File
@@ -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');
});
});
+74
View File
@@ -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');
});
});