Skip to content

Commit a9f47af

Browse files
emiedonmokumoCopilotjasonsaayman
authored
fix(fetch-adapter): set correct Content-Type for Node FormData (#6998)
* fix(fetch-adapter): set correct Content-Type for Node FormData * Update lib/helpers/resolveConfig.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test(fetch): replace chai expect with Node assert * fix: define formHeaders for FormData to resolve no-undef error * fix: filter headers to only update the target headers Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jay <jasonsaayman@gmail.com>
1 parent 066b391 commit a9f47af

2 files changed

Lines changed: 55 additions & 35 deletions

File tree

lib/helpers/resolveConfig.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import buildURL from "./buildURL.js";
1010
export default (config) => {
1111
const newConfig = mergeConfig({}, config);
1212

13-
let {data, withXSRFToken, xsrfHeaderName, xsrfCookieName, headers, auth} = newConfig;
13+
let { data, withXSRFToken, xsrfHeaderName, xsrfCookieName, headers, auth } = newConfig;
1414

1515
newConfig.headers = headers = AxiosHeaders.from(headers);
1616

@@ -23,17 +23,21 @@ export default (config) => {
2323
);
2424
}
2525

26-
let contentType;
27-
2826
if (utils.isFormData(data)) {
2927
if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
30-
headers.setContentType(undefined); // Let the browser set it
31-
} else if ((contentType = headers.getContentType()) !== false) {
32-
// fix semicolon duplication issue for ReactNative FormData implementation
33-
const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
34-
headers.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
28+
headers.setContentType(undefined); // browser handles it
29+
} else if (utils.isFunction(data.getHeaders)) {
30+
// Node.js FormData (like form-data package)
31+
const formHeaders = data.getHeaders();
32+
// Only set safe headers to avoid overwriting security headers
33+
const allowedHeaders = ['content-type', 'content-length'];
34+
Object.entries(formHeaders).forEach(([key, val]) => {
35+
if (allowedHeaders.includes(key.toLowerCase())) {
36+
headers.set(key, val);
37+
}
38+
});
3539
}
36-
}
40+
}
3741

3842
// Add xsrf header
3943
// This is only done if running in a standard browser environment.

test/unit/adapters/fetch.js

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../../helpers/server.js';
1111
import axios from '../../../index.js';
1212
import stream from "stream";
13-
import {AbortController} from "abortcontroller-polyfill/dist/cjs-ponyfill.js";
13+
import { AbortController } from "abortcontroller-polyfill/dist/cjs-ponyfill.js";
1414
import util from "util";
1515

1616
const pipelineAsync = util.promisify(stream.pipeline);
@@ -41,7 +41,7 @@ describe('supports fetch with nodejs', function () {
4141

4242
server = await startHTTPServer((req, res) => res.end(originalData));
4343

44-
const {data} = await fetchAxios.get('/', {
44+
const { data } = await fetchAxios.get('/', {
4545
responseType: 'text'
4646
});
4747

@@ -53,7 +53,7 @@ describe('supports fetch with nodejs', function () {
5353

5454
server = await startHTTPServer((req, res) => res.end(originalData));
5555

56-
const {data} = await fetchAxios.get('/', {
56+
const { data } = await fetchAxios.get('/', {
5757
responseType: 'arraybuffer'
5858
});
5959

@@ -65,7 +65,7 @@ describe('supports fetch with nodejs', function () {
6565

6666
server = await startHTTPServer((req, res) => res.end(originalData));
6767

68-
const {data} = await fetchAxios.get('/', {
68+
const { data } = await fetchAxios.get('/', {
6969
responseType: 'blob'
7070
});
7171

@@ -77,7 +77,7 @@ describe('supports fetch with nodejs', function () {
7777

7878
server = await startHTTPServer((req, res) => res.end(originalData));
7979

80-
const {data} = await fetchAxios.get('/', {
80+
const { data } = await fetchAxios.get('/', {
8181
responseType: 'stream'
8282
});
8383

@@ -104,7 +104,7 @@ describe('supports fetch with nodejs', function () {
104104
res.end(await response.text());
105105
});
106106

107-
const {data} = await fetchAxios.get('/', {
107+
const { data } = await fetchAxios.get('/', {
108108
responseType: 'formdata'
109109
});
110110

@@ -114,11 +114,11 @@ describe('supports fetch with nodejs', function () {
114114
});
115115

116116
it(`should support json response type`, async () => {
117-
const originalData = {x: 'my data'};
117+
const originalData = { x: 'my data' };
118118

119119
server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)));
120120

121-
const {data} = await fetchAxios.get('/', {
121+
const { data } = await fetchAxios.get('/', {
122122
responseType: 'json'
123123
});
124124

@@ -153,8 +153,8 @@ describe('supports fetch with nodejs', function () {
153153

154154
const samples = [];
155155

156-
const {data} = await fetchAxios.post('/', readable, {
157-
onUploadProgress: ({loaded, total, progress, bytes, upload}) => {
156+
const { data } = await fetchAxios.post('/', readable, {
157+
onUploadProgress: ({ loaded, total, progress, bytes, upload }) => {
158158
console.log(`Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`);
159159

160160
samples.push({
@@ -188,10 +188,10 @@ describe('supports fetch with nodejs', function () {
188188
}()));
189189
});
190190

191-
it('should not fail with get method', async() => {
191+
it('should not fail with get method', async () => {
192192
server = await startHTTPServer((req, res) => res.end('OK'));
193193

194-
const {data} = await fetchAxios.get('/', {
194+
const { data } = await fetchAxios.get('/', {
195195
onUploadProgress() {
196196

197197
}
@@ -227,8 +227,8 @@ describe('supports fetch with nodejs', function () {
227227

228228
const samples = [];
229229

230-
const {data} = await fetchAxios.post('/', readable, {
231-
onDownloadProgress: ({loaded, total, progress, bytes, download}) => {
230+
const { data } = await fetchAxios.post('/', readable, {
231+
onDownloadProgress: ({ loaded, total, progress, bytes, download }) => {
232232
console.log(`Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`);
233233

234234
samples.push({
@@ -269,8 +269,8 @@ describe('supports fetch with nodejs', function () {
269269
server = await startHTTPServer((req, res) => res.end(req.headers.authorization));
270270

271271
const user = 'foo';
272-
const headers = {Authorization: 'Bearer 1234'};
273-
const res = await axios.get('http://' + user + '@localhost:4444/', {headers: headers});
272+
const headers = { Authorization: 'Bearer 1234' };
273+
const res = await axios.get('http://' + user + '@localhost:4444/', { headers: headers });
274274

275275
const base64 = Buffer.from(user + ':', 'utf8').toString('base64');
276276
assert.equal(res.data, 'Basic ' + base64);
@@ -279,12 +279,12 @@ describe('supports fetch with nodejs', function () {
279279
it("should support stream.Readable as a payload", async () => {
280280
server = await startHTTPServer();
281281

282-
const {data} = await fetchAxios.post('/', stream.Readable.from('OK'));
282+
const { data } = await fetchAxios.post('/', stream.Readable.from('OK'));
283283

284284
assert.strictEqual(data, 'OK');
285285
});
286286

287-
describe('request aborting', function() {
287+
describe('request aborting', function () {
288288
it('should be able to abort the request stream', async function () {
289289
server = await startHTTPServer({
290290
rate: 100000,
@@ -316,7 +316,7 @@ describe('supports fetch with nodejs', function () {
316316
controller.abort(new Error('test'));
317317
}, 800);
318318

319-
const {data} = await fetchAxios.get('/', {
319+
const { data } = await fetchAxios.get('/', {
320320
responseType: 'stream',
321321
signal: controller.signal
322322
});
@@ -328,7 +328,7 @@ describe('supports fetch with nodejs', function () {
328328
});
329329

330330
it('should support a timeout', async () => {
331-
server = await startHTTPServer(async(req, res) => {
331+
server = await startHTTPServer(async (req, res) => {
332332
await setTimeoutAsync(1000);
333333
res.end('OK');
334334
});
@@ -337,7 +337,7 @@ describe('supports fetch with nodejs', function () {
337337

338338
const ts = Date.now();
339339

340-
await assert.rejects(async() => {
340+
await assert.rejects(async () => {
341341
await fetchAxios('/', {
342342
timeout
343343
})
@@ -358,10 +358,10 @@ describe('supports fetch with nodejs', function () {
358358
assert.equal(res.config.url, '/foo');
359359
});
360360

361-
it('should support params', async() => {
361+
it('should support params', async () => {
362362
server = await startHTTPServer((req, res) => res.end(req.url));
363363

364-
const {data} = await fetchAxios.get('/?test=1', {
364+
const { data } = await fetchAxios.get('/?test=1', {
365365
params: {
366366
foo: 1,
367367
bar: 2
@@ -372,7 +372,7 @@ describe('supports fetch with nodejs', function () {
372372
});
373373

374374
it('should handle fetch failed error as an AxiosError with ERR_NETWORK code', async () => {
375-
try{
375+
try {
376376
await fetchAxios('http://notExistsUrl.in.nowhere');
377377
assert.fail('should fail');
378378
} catch (err) {
@@ -387,10 +387,26 @@ describe('supports fetch with nodejs', function () {
387387
res.end(req.url)
388388
});
389389

390-
const {headers} = await fetchAxios.get('/', {
390+
const { headers } = await fetchAxios.get('/', {
391391
responseType: 'stream'
392392
});
393393

394394
assert.strictEqual(headers.get('foo'), 'bar');
395395
});
396-
});
396+
397+
describe('fetch adapter - Content-Type handling', function () {
398+
it('should set correct Content-Type for FormData automatically', async function () {
399+
const FormData = (await import('form-data')).default; // Node FormData
400+
const form = new FormData();
401+
form.append('foo', 'bar');
402+
403+
server = await startHTTPServer((req, res) => {
404+
const contentType = req.headers['content-type'];
405+
assert.match(contentType, /^multipart\/form-data; boundary=/i);
406+
res.end('OK');
407+
});
408+
409+
await fetchAxios.post('/form', form);
410+
});
411+
});
412+
});

0 commit comments

Comments
 (0)