react-native-fetch-blobをAndroidで使う際に文字化けが起きるケース

React NativeでファイルのアップロードやダウンロードなどAPIを叩く周りの処理を行う際によく使うライブラリ

react-native-fetch-blob

これを使った際に顔文字を使うなど特定の状況下で文字化けが発生しました。(react-native-fetch-blobのバージョン「v0.10.6」でこの現象が起きることを確認しました。)

どうやらUTF8の範囲外の文字が含まれているとレスポンスの文字コードがbase64と判定されてしまっているようでした。

デコードを行ってみても文字化けが解消されないようでした。

処理の流れを追ってみました。

RNFB-Response

react-native-fetch-blobではレスポンスデータの中身を見てUTF8かbase64かの判定をおこなっています。

基本的には自動判定になっていますが、 RNFB-Response のヘッダーを付与することで強制的に文字コードの判定を寄せるのができるようになっています。

https://github.com/wkh237/react-native-fetch-blob/wiki/Fetch-API#rnfb-response-base64–utf8

rnfbEncode

また、レスポンスの文字コードはrnfbEncodeにセットされるようになっています。

https://github.com/wkh237/react-native-fetch-blob/wiki/Classes#rnfbencode–path–base64–utf8

文字化けするケースではここの値が「base64」になっていました。

処理の流れ

エンコード

Androidでのレスポンスデータ取得後の処理を見てみます。

1. java.nio.charset.CharsetEncoderを使ってUTF8でエンコードを行います。UTF8の範囲外の文字が含まれている場合にはExceptionを吐くため、その後catch内の処理に飛びます。

byte[] b = resp.body().bytes();
CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();
if(responseFormat == ResponseFormat.BASE64) {
    callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP));
    return;
}
try {
    encoder.encode(ByteBuffer.wrap(b).asCharBuffer());
    // if the data contains invalid characters the following lines will be
    // skipped.
    String utf8 = new String(b);
    callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, utf8);
}
// This usually mean the data is contains invalid unicode characters, it's
// binary data
catch(CharacterCodingException ignored) {
}

2.もし「RNFB-Response」のヘッダーが指定されている場合には、空文字をセットします。指定されていない場合は文字コードをbase64としてandroid.util.Base64.encodeToStringでエンコードした値をセットします。

 if(responseFormat == ResponseFormat.UTF8) {
     callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, "");
 }
 else {
     callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP));
 }

ここで空文字がセットされているのがつらい・・・

callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, "");

一連の処理の流れ

https://github.com/wkh237/react-native-fetch-blob/blob/5e554ac807a5f167528a8f302b5f53fedc2c5bc3/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java#L470-L486

デコード

次にデコードです。

JSONデータを受け取りたい場合、base64の場合はデコードをしています。エンコード時にはJavaでエンコードしたのにデコード時にはJavascriptでデコードをしています。

 this.json = ():any => {
   switch(this.type) {
     case 'base64':
       return JSON.parse(base64.decode(this.data))
     case 'path':
       return fs.readFile(this.data, 'utf8')
                .then((text) => Promise.resolve(JSON.parse(text)))
     default:
       return JSON.parse(this.data)
   }
 }

https://github.com/wkh237/react-native-fetch-blob/blob/be00a2490802cc57029bde7a5538e6a97517fd23/index.js#L468-L478

さらに悪いことに、デコード時にはUTF8の範囲外の文字が含まれているとエラーになります。

https://github.com/mathiasbynens/base64/blob/master/src/base64.js#L40

解決策

以下の2つの対応を組み合わせることで文字化けが解消されました。

RNFB-Responseに「utf8」を指定する。(UTF8以外の文字が含まれるとbase64でエンコードされるものの、デコード時にUTF8以外の文字が含まれるとエラーになるため、UTF8でもろもろ処理させるように指定します。)

https://github.com/wkh237/react-native-fetch-blob/blob/5e554ac807a5f167528a8f302b5f53fedc2c5bc3/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java#L481

を以下のように変更する(RNFB-Responseに「utf8」を指定しただけだと空になってしまうので、文字列をそのまま渡すように変更する)

callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, "");
↓
callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, new String(b));

参考

https://github.com/wkh237/react-native-fetch-blob/issues/122