diff --git a/PRE_RELEASE_CHANGELOG.md b/PRE_RELEASE_CHANGELOG.md index b182213a..9abc1394 100644 --- a/PRE_RELEASE_CHANGELOG.md +++ b/PRE_RELEASE_CHANGELOG.md @@ -9,6 +9,7 @@ ## Bug Fixes - **AxiosHeaders:** Silently skip empty response header names emitted by some React Native Android responses instead of throwing. (**#6959**, **#10875**) +- **React Native FormData:** Clear the default `Content-Type` header for React Native `FormData` requests so Android can build multipart bodies with the correct boundary. (**#10898**) - **Request Data:** Preserve enumerable symbol keys when merging plain request data before `transformRequest`. (**#6392**) ## Release Documentation TODO diff --git a/lib/helpers/resolveConfig.js b/lib/helpers/resolveConfig.js index 2e9b4b0b..ea9d346a 100644 --- a/lib/helpers/resolveConfig.js +++ b/lib/helpers/resolveConfig.js @@ -70,8 +70,12 @@ function resolveConfig(config) { } if (utils.isFormData(data)) { - if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) { - headers.setContentType(undefined); // browser handles it + if ( + platform.hasStandardBrowserEnv || + platform.hasStandardBrowserWebWorkerEnv || + utils.isReactNative(data) + ) { + headers.setContentType(undefined); // browser/web worker/RN handles it } else if (utils.isFunction(data.getHeaders)) { // Node.js FormData (like form-data package) setFormDataHeaders(headers, data.getHeaders(), own('formDataHeaderPolicy')); diff --git a/tests/unit/core/dispatchRequest.test.js b/tests/unit/core/dispatchRequest.test.js index b2416a3a..11237f59 100644 --- a/tests/unit/core/dispatchRequest.test.js +++ b/tests/unit/core/dispatchRequest.test.js @@ -3,6 +3,19 @@ import assert from 'assert'; import dispatchRequest from '../../../lib/core/dispatchRequest.js'; import AxiosError from '../../../lib/core/AxiosError.js'; import defaults from '../../../lib/defaults/index.js'; +import resolveConfig from '../../../lib/helpers/resolveConfig.js'; + +class ReactNativeFormData { + append() {} + + getParts() { + return []; + } + + get [Symbol.toStringTag]() { + return 'FormData'; + } +} function baseConfig(overrides = {}) { return { @@ -122,6 +135,45 @@ describe('core::dispatchRequest', () => { }); describe('happy path', () => { + it('clears default Content-Type for React Native FormData before adapter headers are sent', async () => { + const data = new ReactNativeFormData(); + const response = { + data: '{"ok":true}', + status: 200, + statusText: 'OK', + headers: {}, + config: null, + request: {}, + }; + const config = baseConfig({ + method: 'post', + data, + adapter: (adapterConfig) => { + assert.strictEqual( + adapterConfig.headers.getContentType(), + 'application/x-www-form-urlencoded', + 'dispatchRequest should apply the default POST Content-Type first' + ); + + const resolvedConfig = resolveConfig(adapterConfig); + + assert.strictEqual(resolvedConfig.data, data); + assert.strictEqual(resolvedConfig.headers.getContentType(), undefined); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(resolvedConfig.headers.toJSON(), 'Content-Type'), + false, + 'resolved adapter headers must omit Content-Type for React Native FormData' + ); + + return Promise.resolve(response); + }, + }); + + const result = await dispatchRequest(config); + + assert.deepStrictEqual(result.data, { ok: true }); + }); + it('cleans up config.response after a successful resolution', async () => { const response = { data: '{"ok":true}', diff --git a/tests/unit/helpers/resolveConfig.test.js b/tests/unit/helpers/resolveConfig.test.js new file mode 100644 index 00000000..9ee95f75 --- /dev/null +++ b/tests/unit/helpers/resolveConfig.test.js @@ -0,0 +1,35 @@ +import { describe, it } from 'vitest'; +import assert from 'assert'; +import resolveConfig from '../../../lib/helpers/resolveConfig.js'; + +class ReactNativeFormData { + append() {} + + getParts() { + return []; + } + + get [Symbol.toStringTag]() { + return 'FormData'; + } +} + +describe('helpers::resolveConfig', () => { + it('clears Content-Type for React Native FormData', () => { + const data = new ReactNativeFormData(); + const config = resolveConfig({ + url: '/upload', + data, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + assert.strictEqual(config.data, data); + assert.strictEqual(config.headers.getContentType(), undefined); + assert.strictEqual( + Object.prototype.hasOwnProperty.call(config.headers.toJSON(), 'Content-Type'), + false + ); + }); +});