capacitor icon indicating copy to clipboard operation
capacitor copied to clipboard

[Bug]: CapacitorHttp breaks axios requests with `multipart/form-data`

Open alex-mironov opened this issue 1 year ago • 14 comments

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

alex-mironov avatar Jul 24 '24 16:07 alex-mironov

Repo with reproduction: https://github.com/alex-mironov/capacitor-axios-issue/blob/main/src/App.tsx

image

alex-mironov avatar Jul 24 '24 18:07 alex-mironov

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.

ionitron-bot[bot] avatar Jul 26 '24 16:07 ionitron-bot[bot]

@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"',
    }
 });

michaelwolz avatar Aug 07 '24 14:08 michaelwolz

Any suggestions on how to approach fixing this issue?

alex-mironov avatar Aug 23 '24 15:08 alex-mironov

The same, any idea how to fix this?

grzegorzCieslik95 avatar Aug 30 '24 11:08 grzegorzCieslik95

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.

JuliaBD avatar Oct 08 '24 17:10 JuliaBD

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)
        }
    }

davideramoaxa avatar Oct 10 '24 13:10 davideramoaxa

If I remove the check about contentType != nil it works (no special boundary needed in my case, just for test)

davideramoaxa avatar Oct 10 '24 14:10 davideramoaxa

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?

davideramoaxa avatar Oct 11 '24 08:10 davideramoaxa

axios version 1.x

davideramoaxa avatar Oct 11 '24 08:10 davideramoaxa

any news about that?

davideramoaxa avatar Nov 07 '24 17:11 davideramoaxa

Any news?

edvijaka avatar Dec 18 '24 13:12 edvijaka

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.

dyazincahya avatar Jan 21 '25 11:01 dyazincahya

(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);
  });

h3786010 avatar Jan 23 '25 02:01 h3786010