React NativeでファイルのアップロードやダウンロードなどAPIを叩く周りの処理を行う際によく使うライブラリ
これを使った際に顔文字を使うなど特定の状況下で文字化けが発生しました。(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