Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch adds charset=utf-8 on Android but not on iOS #8237

Closed
dstaver opened this issue Jun 20, 2016 · 38 comments
Closed

Fetch adds charset=utf-8 on Android but not on iOS #8237

dstaver opened this issue Jun 20, 2016 · 38 comments
Labels
Bug Platform: Android Android applications. Platform: iOS iOS applications. Ran Commands One of our bots successfully processed a command. Resolution: Locked This issue was locked by the bot.

Comments

@dstaver
Copy link

dstaver commented Jun 20, 2016

This fetch request sends "application/json" as ContentType on IOS but "application/json; charset=utf-8" on Android:

fetch(url, {
    method: 'POST',
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
    }
})

The server I'm working against requires application/json and fails when charset is added. I've filed a bug report with them too, but shouldn't fetch behave identically on ios and android?

@etodanik
Copy link

I'm also experiencing this. Having different headers between platforms doesn't make sense and introduces bugs

@lacker
Copy link
Contributor

lacker commented Oct 28, 2016

What's the right thing to do here - should the charset be appended or not?

@himelnagrana
Copy link

Having the same issue while working with Urban Airship APIs. Frustrating as UA supposed to be not changing at their end. What to do?

@nalejandroveron
Copy link

Im also seeing this

@ide
Copy link
Contributor

ide commented Mar 2, 2017

What's the right thing to do here - should the charset be appended or not?

Looking at Chrome, it doesn't modify the content-type header at all so we shouldn't be adding a charset.

@alterx
Copy link

alterx commented Apr 20, 2017

I'm also experiencing this issue, somehow charset=utf-8 is added as a parameter to the Content-Type header. It only happens on Android. Also, not using fetch but the XMLHttpRequest polyfill that comes with ReactNative by default.

In my specific case, I'm interacting with a JSON API (http://jsonapi.org/format/) and adding media type parameters causes the server to respond with a 415 error every time. This pretty much breaks the app :(

Edit:

I've debugged deep down into react-native/Libraries/BatchedBridge/NativeModules.js:


function genMethod(moduleID: number, methodID: number, type: MethodType) {
  let fn = null;
  if (type === 'promise') {
    fn = function(...args: Array<any>) {
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(moduleID, methodID, args,
          (data) => resolve(data),
          (errorData) => reject(createErrorFromErrorData(errorData)));
      });
    };
  } else if (type === 'sync') {
    fn = function(...args: Array<any>) {
      return global.nativeCallSyncHook(moduleID, methodID, args);
    };
  } else {
    fn = function(...args: Array<any>) {
      const lastArg = args.length > 0 ? args[args.length - 1] : null;
      const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
      const hasSuccessCallback = typeof lastArg === 'function';
      const hasErrorCallback = typeof secondLastArg === 'function';
      hasErrorCallback && invariant(
        hasSuccessCallback,
        'Cannot have a non-function arg after a function arg.'
      );
      const onSuccess = hasSuccessCallback ? lastArg : null;
      const onFail = hasErrorCallback ? secondLastArg : null;
      const callbackCount = hasSuccessCallback + hasErrorCallback;
      args = args.slice(0, args.length - callbackCount);
      BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
    };
  }
  fn.type = type;
  return fn;
}

and, even here the Content-Typeheader remains untouched what leads me to think that is the native code in Android what's adding the charset=utf-8

@laurent22
Copy link

I'm also having this problem and, while doing some testing, I've tried setting an invalid charset "dropbox-cors-hack" (as I'm testing the Dropbox API) so the request was like this:

apiRequest = request.post(getBaseURL(host) + path)
  //.type('application/octet-stream;')
  .type('text/plain; charset=dropbox-cors-hack')
  .set('Authorization', 'Bearer ' + accessToken)
  .set('Dropbox-API-Arg', httpHeaderSafeJson(args));

When doing so I got the following exception:

And that lead me to this file from the okhttp library:

https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/RequestBody.java#L53

So it seems it appends "charset=utf-8" if the charset is not set. I'm not sure if this is React-Native that is calling this lib or if it's Android, but is there anything that can be done to prevent this behaviour?

@laurent22
Copy link

Based on this comment it doesn't look like it will be fixed on the okhttp side:

square/okhttp#3081

They suggest using the create overload that takes a byte[] parameter. Can it be done at the React-Native level?

@Knight704
Copy link

Knight704 commented Aug 3, 2017

@laurent22 you could customize okhttp client on android that is used under the hood to implement fetch. You can add interceptor to this client and modify any header before it is being processed.
Unfortunately, current version of react-native doesn't give you an easy way to provide custom okhttp client, but here I posted a way to workaround this.

@hey99xx
Copy link

hey99xx commented Aug 10, 2017

@Knight704 our discussion in other thread was about keeping a single OkHttp instance. I don't understand how that helps to fix this behavior in OkHttp. Are you overriding entire NetworkingModule to call RequestBody.create with (MediaType, byte[]) arguments? Otherwise did you figure out a way to override the OkHttp default utf-8 charset append behavior, because it's a static method that adds the charset type?

@Knight704
Copy link

Knight704 commented Aug 10, 2017

@hey99xx the idea boils down to put your custom interceptor to okhttpclient. Yes, okhttp will continue appending charset into header, but inside interceptor you can modify any headers before it really goes to the network, i.e cutting down charset part. This is of course a workaround, but it'll work. Another point that okhttp isn't easy to access right now, so I provided link to solution to that problem as well

@hey99xx
Copy link

hey99xx commented Aug 11, 2017

@Knight704 How would you know if the "Content-type" is automatically set to have the charset part by OkHttp, and not by manually adding the header during fetch call? I mean it's an acceptable solution, but it comes with few questions. Ideally it should be fixed in one of react-native or OkHttp so we happily keep developing our apps.

On my end, I've managed to fix the issue by contacting the backend people to be tolerant to the charset header, but I don't think that's doable when you contact external APIs such as AWS and Dropbox.

@Knight704
Copy link

Knight704 commented Aug 11, 2017

@hey99xx yeah, I've already mentioned that this is just a hack, and preferably it should be fixed in SDK.
Nevertheless, if you are developing your app you should know endpoints where charset should (or shoudn't) be added and may have black/white list that is checked inside interceptor, there are many ways for this, the first that came to my mind was to put your custom header like 'no-charset-append' on js side and cut down this appending if this header exists on native side (of course removing this 'hack' header as well). Ugly, but flexible and would work

@jeremy8883
Copy link

jeremy8883 commented Sep 28, 2017

For anyone wanting a copy/paste example, follow the steps outlined here (thanks heaps for that by the way @Knight704), then you can copy my version of NetworkingModuleUtils:

package com.facebook.react.modules.network;

import com.facebook.react.bridge.ReactApplicationContext;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class NetworkingModuleUtils {
    public static NetworkingModule createNetworkingModuleWithCustomClient(ReactApplicationContext context) {
        OkHttpClient client = OkHttpClientProvider.createClient();

        OkHttpClient customClient = client.newBuilder()
            .addNetworkInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request request = chain.request();
                    String contentType = request.header("Content-Type");
                    Request customRequest = "application/json; charset=utf-8".equals(contentType) ?
                        request.newBuilder()
                            .header("content-type", "application/json")
                            .build() : request;

                    Response response = chain.proceed(customRequest);
                    return response;
                }
            }).build();

        return new NetworkingModule(context, null, customClient);
    }
}

@Kottidev
Copy link

@jeremy8883, in which directory i copy and past your code ? in src/java/com/ ?

your class resolve the problem for appending "charset=utf-8" to content-type ?

@jeremy8883
Copy link

jeremy8883 commented Oct 11, 2017

in which directory i copy and past your code ? in src/java/com/ ?

Yeah, plus the package location, so src/main/java/com/facebook/react/modules/network/NetworkModuleUtils.java. Make sure you also follow the steps in this link.

your class resolve the problem for appending "charset=utf-8" to content-type ?

That's correct

@ceefour
Copy link

ceefour commented Dec 9, 2017

I use Content-Type: image/jpeg and it becomes Content-Type: image/jpeg; charset=utf-8 which obviously doesn't make sense. :(

I agree with @laurent22 , can we change Fetch implementation to use okhttp's byte[] overload?
Fetch API should be consistent on all platforms, not to mention that automatically adding charset breaks various stuff.

Note: @Knight704 's hack won't work with CRNA/Expo due to unable to access Android code

@stale
Copy link

stale bot commented Feb 7, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions.

@stale stale bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Feb 7, 2018
@etodanik
Copy link

etodanik commented Jun 17, 2018 via email

@stale stale bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Jun 17, 2018
@archer56
Copy link

+1

@limbuster
Copy link

This is still an issue

@programadornatal
Copy link

To resolve this problem, use lib axios
https://github.com/qiangmao/axios

@jeremy8883
Copy link

Axios is a javascript library. The problem is in the RN/okhttp java implementation.

@programadornatal
Copy link

Axios work for me in IOS

@ceefour
Copy link

ceefour commented Oct 8, 2018

@programadornatal iOS doesn't have this bug even using pure fetch, without using axios. This is Android specific.

@glenndebacker
Copy link

Axios has the same problem on Android. There is a topic describing the same problem but the solution of changing the default header does not seem to work. Android (or RN) seems to be modifying it on a lower level.

@nhunzaker
Copy link
Contributor

This seemed like a fun one to dig into, so I proposed a fix here:
#23580

facebook-github-bot pushed a commit that referenced this issue Feb 22, 2019
Summary:
Before this commit, `fetch()` calls append `"charset=utf8"` to the `Content-Type` header on Android (and not on iOS). This is because of an implementation detail in the okhttp library. This means that you can make a call on the JavaScript side like:

```javascript
let body = JSON.stringify({ key: "value" });

let headers = {
  "Content-Type": "application/json"
};

fetch("http://10.0.2.2:3000", { method: "POST", body, headers });
```

However the resulting request appends the utf8 character:

```
POST - 13:34:32:
  content-type: application/json; charset=utf-8
  content-length: 15
  host: 10.0.2.2:3000
  connection: Keep-Alive
  accept-encoding: gzip
  user-agent: okhttp/3.12.1
```

Passing byte array into the RequestBody avoids this, as recommended by a maintainer of okhttp:

square/okhttp#2099 (comment)

Related issues:
#8237

[Android][fixed] - Prevent fetch() POST requests on Android from appending `charset=utf-8` to `Content-Type` header.
Pull Request resolved: #23580

Differential Revision: D14180849

Pulled By: cpojer

fbshipit-source-id: b84cadf83361331a9f64d1ff5f2e6399a55527a6
@hey99xx
Copy link

hey99xx commented Feb 22, 2019

@nhunzaker I know I approved your PR but just looking again, I wanted to clarify this. I cannot ask on the PR itself since it's locked already. You had mentioned

I wondered about that, or passing in the character set as defined by MediaType.parse.

Do you still think it'd be better? One use case that came to my mind is setting a custom content-type header like: Content-Type: text/plain; charset=utf-16.

In that case OkHttp's RequestBody.create would have likely used utf-16 charset to get the bytes. Now after this change we'd be sending bytes encoded with utf-8, but the charset in the header does not match, potentially confusing the servers.

I really don't know what the correct implementation is supposed to be. Maybe only default to utf-8 is MediaType is null or charset-less? That's what OkHttp internally does.

@hey99xx
Copy link

hey99xx commented Feb 22, 2019

Example of how this failure could be> I've never written a raw http server though so cant tell if this is how it works:

String s = "Friðjónsson";
Charset UTF_8 = StandardCharsets.UTF_8;
Charset UTF_16 = StandardCharsets.UTF_16;
System.out.println("original: " + s);
System.out.println("utf8: " + new String(s.getBytes(UTF_8), UTF_8));
System.out.println("utf16: " + new String(s.getBytes(UTF_16), UTF_16));
System.out.println("utf8/16: " + new String(s.getBytes(UTF_8), UTF_16));

output is:

original: Friðjónsson
utf8: Friðjónsson
utf16: Friðjónsson
utf8/16: 䙲槃끪쎳湳獯�

@nhunzaker
Copy link
Contributor

@hey99xx you are correct. my implementation needs to respect the existing character set. I have a test case that reproduces the issue, I'll send out another PR. The code I changed needs to really be something like:

Charset charset = contentMediaType.charset();

if (charset == null) {
  charset = StandardCharsets.UTF_8;
}

requestBody = RequestBody.create(contentMediaType, body.getBytes(charset));

nhunzaker added a commit to nhunzaker/react-native that referenced this issue Feb 22, 2019
This commit fixes a bug introduced in a previous attempt to address an
issue where okhttp appended `charset=utf-8` to the Content-Type header
when otherwise not specified.

In that commit, I converted all characters to UTF-8, however it should
instead use an existing encoding when possible.

Related issues:
facebook#8237 (comment)
facebook-github-bot pushed a commit that referenced this issue Feb 23, 2019
Summary:
This commit fixes a bug introduced in a previous attempt (#23580) to address an issue where okhttp appended `charset=utf-8` to the Content-Type header when otherwise not specified.

In that commit, I converted all characters to UTF-8, however it should instead use an existing encoding when possible.

Related issues:
#8237 (comment)

[Android][fixed] - Respect existing character set when specified in fetch() POST request
Pull Request resolved: #23603

Differential Revision: D14191750

Pulled By: hramos

fbshipit-source-id: 11c1bfd98ccd33cd8e54ea426285b7d2ce9c2d7c
grabbou pushed a commit that referenced this issue Feb 27, 2019
Summary:
Before this commit, `fetch()` calls append `"charset=utf8"` to the `Content-Type` header on Android (and not on iOS). This is because of an implementation detail in the okhttp library. This means that you can make a call on the JavaScript side like:

```javascript
let body = JSON.stringify({ key: "value" });

let headers = {
  "Content-Type": "application/json"
};

fetch("http://10.0.2.2:3000", { method: "POST", body, headers });
```

However the resulting request appends the utf8 character:

```
POST - 13:34:32:
  content-type: application/json; charset=utf-8
  content-length: 15
  host: 10.0.2.2:3000
  connection: Keep-Alive
  accept-encoding: gzip
  user-agent: okhttp/3.12.1
```

Passing byte array into the RequestBody avoids this, as recommended by a maintainer of okhttp:

square/okhttp#2099 (comment)

Related issues:
#8237

[Android][fixed] - Prevent fetch() POST requests on Android from appending `charset=utf-8` to `Content-Type` header.
Pull Request resolved: #23580

Differential Revision: D14180849

Pulled By: cpojer

fbshipit-source-id: b84cadf83361331a9f64d1ff5f2e6399a55527a6
grabbou pushed a commit that referenced this issue Feb 27, 2019
Summary:
This commit fixes a bug introduced in a previous attempt (#23580) to address an issue where okhttp appended `charset=utf-8` to the Content-Type header when otherwise not specified.

In that commit, I converted all characters to UTF-8, however it should instead use an existing encoding when possible.

Related issues:
#8237 (comment)

[Android][fixed] - Respect existing character set when specified in fetch() POST request
Pull Request resolved: #23603

Differential Revision: D14191750

Pulled By: hramos

fbshipit-source-id: 11c1bfd98ccd33cd8e54ea426285b7d2ce9c2d7c
@hey99xx
Copy link

hey99xx commented Feb 27, 2019

The fixes are in 0.59.0-rc.3 which is just released. Can someone verify the problem is gone and we can then resolve this issue?

@bartolkaruza
Copy link

Confirmed that this works on RN 0.59.1!

@facebook facebook locked as resolved and limited conversation to collaborators Mar 20, 2020
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Mar 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug Platform: Android Android applications. Platform: iOS iOS applications. Ran Commands One of our bots successfully processed a command. Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests