ionic-team / capacitor

Build cross-platform Native Progressive Web Apps for iOS, Android, and the Web ⚡️
https://capacitorjs.com
MIT License
12.3k stars 1.01k forks source link

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

Open alex-mironov opened 4 months ago

alex-mironov commented 4 months ago

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

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 commented 4 months ago

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

image
ionitron-bot[bot] commented 4 months ago

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.

michaelwolz commented 3 months ago

@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"',
    }
 });
alex-mironov commented 3 months ago

Any suggestions on how to approach fixing this issue?

grzegorzCieslik95 commented 2 months ago

The same, any idea how to fix this?

JuliaBD commented 1 month ago

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.

davideramoaxa commented 1 month ago

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 commented 1 month ago

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

davideramoaxa commented 1 month ago

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 commented 1 month ago

axios version 1.x

davideramoaxa commented 2 weeks ago

any news about that?