[Bug]: CapacitorHttp breaks axios requests with `multipart/form-data`
Capacitor Version
Latest Dependencies:
@capacitor/cli: 6.1.1
@capacitor/core: 6.1.1
@capacitor/android: 6.1.1
@capacitor/ios: 6.1.1
Installed Dependencies:
@capacitor/cli: 6.1.1
@capacitor/core: 6.1.1
@capacitor/android: 6.1.1
@capacitor/ios: 6.1.1
Other API Details
No response
Platforms Affected
- [X] iOS
- [X] Android
- [ ] Web
Current Behavior
axios request with Content-Type: "multipart/form-data" is not working. It doesn't send neither Content-Length, Content-Type no content itself.
It happens when CapacitorHttp plugin is enabled.
I have a simple form allowing uploading files.
<input
title="Attach files"
type="file"
accept="image/*"
multiple
onChange={handleChange}
/>
and handler
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
const fileFirst = files?.item(0)
if (!fileFirst) return
const formData = new FormData();
formData.append('file', fileFirst);
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
});
console.log('File uploaded successfully:', response.status);
};
capacitor.config.ts
import { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
// ....
plugins: {
CapacitorHttp: {
enabled: true,
},
// ...
},
};
export default config;
Expected Behavior
axios should work no matter if CapacitorHttp enabled or not
Project Reproduction
Additional Information
No response
Repo with reproduction: https://github.com/alex-mironov/capacitor-axios-issue/blob/main/src/App.tsx
This issue has been labeled as type: bug. This label is added to issues that that have been reproduced and are being tracked in our internal issue tracker.
@jcesarmobile I am relatively sure that this should also be fixed by #7518. The example by @alex-mironov did not set a boundary and currently a fallback for this is missing (see: https://github.com/ionic-team/capacitor/pull/7518/files#diff-8f913b48ce428d2f82c671b3331c5b9efacd6babec8c719dea09dbf17c28c79dR233). However, I think Android automatically did set a fallback already.
@alex-mironov you could try manually adding a boundary to your call which should be a workaround for the moment:
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data; boundary="foo"',
}
});
Any suggestions on how to approach fixing this issue?
The same, any idea how to fix this?
I have a very similar issue when doing a POST request with formData, using the @angular/common/http HttpClient, and with the CapacitorHttp plugin enabled.
It was working fine (200 results), until I upgraded to Capacitor 6 (tried different versions, all 500 results only), without making any other code changes.
The only change I can see in the request logged by CapacitorHttp is that the headers for Capacitor 5 (= 200 result) include "Content-Type": "multipart/form-data; boundary=--1728408087149", whereas this line is missing for Capacitor 6 (= 500 result).
Capacitor 5 - CapacitorHttp request, with 200 result:
{
"callbackId": "8641076",
"pluginId": "CapacitorHttp",
"methodName": "request",
"options": {
"url": "https://loremipsum”,
"method": "POST",
"data": [
{
"key": "answeredSurvey",
"value": "{\"id\":511,\"isChecked\":false}",
"type": "string"
}
],
"headers": {
// the only line that differs from breaking request below:
"Content-Type": "multipart/form-data; boundary=--1728408087149",
"Accept": "application/json, text/plain, */*"
},
"dataType": "formData"
}
}
Capacitor 6 - CapacitorHttp request, with 500 result:
{
"callbackId": "29334309",
"pluginId": "CapacitorHttp",
"methodName": "request",
"options": {
"url": "https://loremipsum”,
"method": "POST",
"data": [
{
"key": "answeredSurvey",
"value": "{\"id\":511,\"isChecked\":false}",
"type": "string"
}
],
"headers": {
"Accept": "application/json, text/plain, */*"
},
"dataType": "formData"
}
}
Here is part of my post function, very simple, no headers specified:
const json = JSON.stringify(answeredSurvey);
const formData = new FormData();
formData.append(‘answeredSurvey’, json);
return this.http.post<PostResponse>(url, formData);
When I include headers in my request with "Content-Type": "multipart/form-data" this does not work, as no boundary is added/extracted.
Is it possible that Capacitor 6 removes the Content-Type before executing the fetch?
core-plugin.ts -> buildRequestInit line 424
headers.delete('content-type'); // content-type will be set by `window.fetch` to includy boundary
Besides the Content-Type header is needed (at least in iOS) to set request body:
CapacitorUrlRequest.m line 191
public func setRequestBody(_ body: JSValue, _ dataType: String? = nil) throws {
let contentType = self.getRequestHeader("Content-Type") as? String
if contentType != nil {
request.httpBody = try getRequestData(body, contentType!, dataType)
}
}
If I remove the check about contentType != nil it works (no special boundary needed in my case, just for test)
Further investigations:
Debugging axios code it seems that the lib/helpers/resolveConfig.js
https://github.com/axios/axios/blob/17cab9c2962e6fb1b7342a8b551f944b95554fdf/lib/helpers/resolveConfig.js#L30
clear out the Content-Type.
Not sure if it's correct or not, but for sure something is broken.
The result is that CapacitorHttp receives no Content-Type and this prevents the request.httpBody setting.
Can somebody explain why the browser is expected to set the Content-Type and why this does not happen?
axios version 1.x
any news about that?
Any news?
Up this issue
everything worked fine when I was using Capacitor version 5. However, after trying Capacitor versions 6 and 7, I encountered issues with handling multipart/form-data. Initially, I intended to upgrade to the newer version, but I decided to revert to version 5 (the older version) as it proved to be more stable.
(Capacitor 6) set CapacitorHttp.enabled=true (bypass CORS issues) Use the Web Fetch API to test successful file uploads on the Web and Android platforms.
await fetch($URL, {
method: 'POST',
body: formData,
}).then((response) => {
alert(response.status);
}).catch((error) => {
alert(error.message);
});