facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.4k stars 24.36k forks source link

Cannot Make API call to http endpoint when using formData[Tried with both fetch and axios] #44737

Open H3tansh opened 5 months ago

H3tansh commented 5 months ago

Description

Cannot Make API call to http endpoint when using formData[Tried with both fetch and axios]

I've tried addring clearTextTrafic true in android Mainfest.xml file

Working fine in iOS, and working well with normal APIs ( without formData), only API with formData having this issue.

Steps to reproduce

const formData = new FormData() formData.append('audio_file', { name: fileName, type: 'audio/mp3', uri: filePath })

await fetch('http endpoint', {
    body: formData,
    method: 'POST',
    headers: {
        'Content-Type': 'multipart/form-data',
        Authorization:
            'Bearer token'
    }
})
    .then(res => {
        console.log('test result ', res)
        console.log('test result status ', res.status)
    })
    .catch(e => {
        console.log('test e ', e)
    })

This returns network error

React Native Version

0.74.1

Affected Platforms

Runtime - Android

Output of npx react-native info

REACT-NATIVE-INFO output:-

System:
OS: macOS 14.2.1
CPU: (8) arm64 Apple M1
Memory: 599.88 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 18.19.0
path: ~/.nvm/versions/node/v18.19.0/bin/node
Yarn:
version: 3.6.4
path: /opt/homebrew/bin/yarn
npm:
version: 10.2.3
path: ~/.nvm/versions/node/v18.19.0/bin/npm
Watchman:
version: 2024.04.15.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.15.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 23.0
- iOS 17.0
- macOS 14.0
- tvOS 17.0
- watchOS 10.0
Android SDK:
API Levels:
- "27"
- "28"
- "29"
- "30"
- "31"
- "32"
- "33"
- "34"
Build Tools:
- 27.0.3
- 28.0.3
- 29.0.2
- 29.0.3
- 30.0.2
- 30.0.3
- 31.0.0
- 32.0.0
- 32.1.0
- 33.0.0
- 33.0.0
- 33.0.1
- 34.0.0
System Images:
- android-32 | Google APIs ARM 64 v8a
- android-33 | Google APIs ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2023.1 AI-231.9392.1.2311.11330709
Xcode:
version: 15.0/15A240d
path: /usr/bin/xcodebuild
Languages:
Java:
version: 21.0.2
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.74.1
wanted: 0.74.1
react-native-macos: Not Found
npmGlobalPackages:
"react-native": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false

Stacktrace or Logs

Instance Create Error [AxiosError: Network Error]

Reproducer

private repo cannot provide link

Screenshots and Videos

No response

github-actions[bot] commented 5 months ago
:warning: Missing Reproducible Example
:information_source: We could not detect a reproducible example in your issue report. Please provide either:
  • If your bug is UI related: a Snack
  • If your bug is build/update related: use our Reproducer Template. A reproducer needs to be in a GitHub repository under your username.
Ahmad-Elsayed commented 5 months ago

Hello,

I've come across your issue and would like to suggest a couple of potential solutions that might resolve the problem you're experiencing with FormData on Android.

Firstly, ensure that the URI for the file you're appending to FormData has the correct scheme. On Android, the file URI should start with file://. Additionally, verify that the MIME type is accurate. For audio files, instead of 'audio/mp3', you should use 'audio/mpeg'.

Here's an updated snippet of your code with these changes:


const formData = new FormData();
formData.append('audio_file', {
  name: fileName,
  type: 'audio/mpeg', // Correct MIME type
  uri: `file://${filePath}` // Correct URI scheme
});

try {
  const response = await fetch('http endpoint', {
    body: formData,
    method: 'POST',
    headers: {
      'Content-Type': 'multipart/form-data',
      Authorization: 'Bearer token'
    }
  });
  console.log('test result ', response);
  console.log('test result status ', response.status);
} catch (e) {
  console.error('test error ', e);
}
H3tansh commented 5 months ago

accurate

Tried everything including your suggestion, still no luck 😓

chr4ss12 commented 4 months ago

I don't know if this helps OR is related, but we have been using FormData for 4+ years, and it broke when upgrading to 0.74.3 from 0.72, the culprit being

https://github.com/facebook/react-native/blob/e553acaf1e64d8829e584b209fa6509b45fba498/packages/react-native/Libraries/Network/FormData.js#L51

addition of

headers['content-disposition'] += `; filename="${
            value.name
          }"; filename*=utf-8''${encodeURI(value.name)}`;

specifically filename* part, so I've had to patch-package and revert it for time being.

I kept getting "request is malformed". YOu can debug network errors a bit better using Charles and SSL stripping, so you will see exactly what is being sent/received as well the format etc.

https://github.com/facebook/react-native/commit/7c7e9e6571c1f702213e9ffbb40921cd5a1a786b

@robertying

robertying commented 4 months ago

@chr4ss12 Not sure why you mentioned me. This change of filename* added support for non-ascii filenames, and it's suggested to keep both filename= and filename*= in the header value for maximum compatibility. As the MDN doc says, "when both filename and filename are present in a single header field value, filename is preferred over filename when both are understood."

Check the encoded name. If the filename*= part is correctly encoded, but you still get errors from the request, it might be an issue with the HTTP server that's handling the header.

chr4ss12 commented 4 months ago

@robertying I saw you created the commit (sorry if that's not the case - and you only approved it). I am using very simple code to upload files to google cloud storage, and it stopped working after that commit, there's nothing I can do with the HTTP server that's handling the header (the name= is always hardcoded to "myfile.jpg"),

just giving heads up to anyone else experiencing this issue.

robertying commented 4 months ago

@chr4ss12 I did make the change. This change was necessary to support utf-8 filenames.

If you could have a repro or try constructing the same header value in a form data request in Postman to isolate it, and see if it fixes the issue, it would be easier to see where it went wrong. I previously tested the multipart upload with Express.js and Multer, they received files fine.

chr4ss12 commented 4 months ago

@robertying I've checked the code, the spec, played around the request in Charles, and everything checks out - as far as I can tell it looks all right.

The problem is with google Cloud storage not knowing how to read the filename*=UTF-8''dummy.jpg part of the request - I will try and see if I can raise an issue in google tracker.

In meanwhile, am not too convinced of this change though, mainly because the way they implemented the syntax in the first place:

filename*=utf-8''

I. mean really? did they not want to throw in anything else that for sure will break ANY implementation that has no filename*= parsing support, unless that was their idea to begin with...

Also the app I use 'Charles' which I use for debugging could not parse the multipart data because it thought it is malformed (it is not), it seems this filename*= syntax is not that well adapted, perhaps something more along side of lines

if (name contains non ascii characters) //emit  the filename*=
robertying commented 4 months ago

@chr4ss12 Yes in theory if the parser doesn't know the keys in the header value, it should throw them away, according to the doc. In practice though, I can definitely see implementations that don't conform to the spec.

Feel free to file a bug and propose a fix in a PR.

Thank you for investigating this. 🙏

Tadimsky commented 3 months ago

We experienced this exact same issue due to this bug - it looks like it's either that we're not encoding the name correctly or the server cannot parse this value. In our case, our server is Go-based and will not parse the filename provided if there are any parentheses in the filename.

This is an example of a header that we're getting from React Native now, which is not being parsed correctly by Go.

form-data; name="1"; filename="84fb1493-321f-471f-9c63-7e98019b2931 (1).pdf"; filename*=utf-8''84fb1493-321f-471f-9c63-7e98019b2931 (1).pdf

This is the go stdlib for mime parsing: https://cs.opensource.google/go/go/+/refs/tags/go1.22.6:src/mime/mediatype.go;l=161-169

Do we know if encodeURI is valid per the RFC to encode a filename in utf-8? https://github.com/facebook/react-native/commit/7c7e9e6571c1f702213e9ffbb40921cd5a1a786b#diff-756cfe2421bc80e4c12e447a744cff5190da329b1a59614a7ebd853873f6a741R87

Should we not be using encodeURIComponent instead?

Compared to encodeURI(), this function encodes more characters, including those that are part of the URI syntax.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent

robertying commented 3 months ago

@Tadimsky encodeURIComponent doesn't encode parentheses () either.

Maybe check if the server can parse the header if you get rid of the filename*= part.

Tadimsky commented 3 months ago

Ah you're right, sorry about that!

It looks like MDN has an example of how to create RFC-5987 valid values for Content-Disposition which is a combination of encodeURIComponent and some custom replacements.

I think we should probably be using something like that in React Native?

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_content-disposition_and_link_headers

robertying commented 3 months ago

@Tadimsky Thanks so much for finding that. Would you like to raise a PR with that?

Tadimsky commented 3 months ago

Yes, let me give that a try!

foyarash commented 2 months ago

Hello,

I just encountered the same problem with the Content-Disposition, and came onto saw this issue. After some digging into the MDN docs, i found that this section states that the filename* should not be there for a Content-Disposition thats related to a form-data.

Note that the request header does not have the filename* parameter and does not allow RFC 5987 encoding.

I hope I'm understanding that correctly. I can create a PR for this if necessary.

Igornorlin commented 2 months ago

Hello,

I just encountered the same problem with the Content-Disposition, and came onto saw this issue. After some digging into the MDN docs, i found that this section states that the filename* should not be there for a Content-Disposition thats related to a form-data.

Note that the request header does not have the filename* parameter and does not allow RFC 5987 encoding.

I hope I'm understanding that correctly. I can create a PR for this if necessary.

My understanding is exactly that as well.

This issue has caused unexpected issues for us too.

m4a1carbin4 commented 2 months ago

Hello, I Think i'm having the same problem here.

async postMultipart(url: string, data: any) {
        const formData = new FormData();
        let imageUrl: string = '';

        Object.keys(data).map(key => {
            console.log(key);
            const value = data[key];

            if (key == 'tags' || key == 'image') {
                console.log('tags value will be ignored');
            } else if (key == 'imageString') {
                imageUrl = value;
            } else if (key == 'aiGenerated') {
                formData.append('ai_generated', value);
            } else if (Array.isArray(value)) {
                value.forEach(item => formData.append(`${key}`, item));
            } else {
                if (value === '') {
                    formData.append(key, 'none');
                } else {
                    formData.append(key, value);
                }
            }

            return true;
        });

        const imageBlob = await loadImageFile(imageUrl);
        const extension = imageBlob.type.split('/')[1];
        const fileName = imageUrl.split('/').pop();
        console.log('value');
        const file = await new File([imageBlob], fileName + '.' + extension);
        console.log(file);
        await formData.append('image', file, fileName + '.' + extension);
        /*await formData.append('image', {
            uri: imageUrl,
            name: file.name,
            type: file.type,
        });*/
        console.log('formData', formData);

        const token = await getPersistData(DataPersistKeys.TOKEN);

        const response = await fetch(this.config.url + url, {
            method: 'POST',
            headers: {
                'Content-Type': 'multipart/form-data',
                Authorization: `Bearer ${token}`,
            },
            body: formData,
        });

        /*await this.axiosInstance.post(url, formData, {
            headers: {
                'Content-Type': 'multipart/form-data',
            },
            transformRequest: (data, headers) => {
                return data;
            },
        }); */

        return response;
    }

Even when using the code that has been commented out, a network error keep occurs. (both fetch and axios) image