JavaScript
JSON
UTF-8
Ajax
Shift_JIS

Ajax&JSONでUTF-8以外のエンコーディングを使用する(クライアントサイド編)

Ajaxでの「Content-Type: application/json」の処理において、UTF-8以外のエンコーディングを使用することに予想外に苦労したため、調査・実装した内容をまとめようと思います。


UTF-8以外のエンコーディングの使用はかなり稀、かつ、極力避けるべきと思います(*1)が、

古いShift_JIS主体のシステムと連携しなければならない場合など、

当記事の内容が役立つこともあるかもしれません。

当記事ではクライアントサイド(javascript)について紹介し、

サーバーサイド(java Servlet&SpringMVC)については別の記事で紹介します。


デモ用ソースコード

実装のポイントは以下の通りです。ソースコードはGitHubにもアップロードしています。

(a). リクエストボディのJSONは、エンコーディング用のライブラリを使用し自前でエンコードする

(b). sendメソッドの引数bodyはBlob型にする

(c). 「responseType」は「text」のままとする

(d). レスポンスのContent-Typeヘッダーフィールドにcharsetが明示されていない場合、かつ、レスポンスボディのエンコーディングがUTF-8でない場合「overrideMimeType」メソッドでcharsetを明示したContent-Typeを指定する

<!DOCTYPE html>

<html>
<head>
<meta charset="UTF-8"/>
</head>
<body>
param1: <input type="text" value="200" id="param1"/><br/>
param2: <input type="text" value="こんにちは" id="param2"/><br/>
charset(request): <select name="charset_request" id="charset_request">
<option value="UTF-8">UTF-8</option>
<option value="Shift_JIS">Shift_JIS</option>
<option value="EUC-JP">EUC-JP</option>
</select>
charset(response):
<select name="charset_response" id="charset_response">
<option value="UTF-8">UTF-8</option>
<option value="Shift_JIS">Shift_JIS</option>
<option value="EUC-JP">EUC-JP</option>
</select><br/>
<button type="button" id="send_button_servlet" data-url='/JISAjaxJSONSample/servlet/jis/showResult'>
送信(servlet)
</button><br/>
<button type="button" id="send_button_spring" data-url='/JISAjaxJSONSample/app/jis/showResult'>
送信(spring mvc)
</button>

<script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/1.0.29/encoding.js"></script>
<script>

function postJSON() {

// 動作確認をしやすくするため、セレクトボックスからエンコーディングを選択できるようにしています
var requestCharsetSelect = document.getElementById('charset_request');
var requestCharset = requestCharsetSelect.options[requestCharsetSelect.selectedIndex].value;

var responseCharsetSelect = document.getElementById('charset_response');
var responseCharset = responseCharsetSelect.options[responseCharsetSelect.selectedIndex].value;

var requestBody = JSON.stringify({
param1: document.getElementById('param1').value,
param2: document.getElementById('param2').value
});

// (a)
// JSON文字列を指定されたエンコーディングに変換します。
// encoding.jsに対してエンコーディングを指定する文字列は、encoding.jsのドキュメントに従います。
// https://github.com/polygonplanet/encoding.js/blob/master/README_ja.md
var charsetNameMapping = {
'Shift_JIS': 'SJIS',
'UTF-8': 'UTF8',
'EUC-JP': 'EUCJP',
};
var encodedRequestBody = Encoding.convert(requestBody, {
from: 'UNICODE',
to: charsetNameMapping[requestCharset],
type: 'arraybuffer'
});

// (b)
// リクエストボディとして送信するBlobを生成します。
// 送信時に、指定したcharsetがContent-Typeヘッダに付与されるよう「type」を明示します。
var mimeType = 'application/json; charset=' + requestCharset;
var blobRequestBody = new Blob([new Uint8Array(encodedRequestBody)], {type: mimeType});

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.status === 200 && this.readyState === this.DONE) {
// (d)
// レスポンスのContent-Typeヘッダのcharsetでエンコーディングが判断されます。
// レスポンスのContent-Typeヘッダにcharsetが明示されていない場合、
// かつ、レスポンスボディのエンコーディングがUTF-8でない場合、
// 「overrideMimeType」メソッドを使ってcharsetを明示することで対応できます。
var responseJSON = JSON.parse(this.responseText);
alert('result1=' + responseJSON.result1 + ', result2=' + responseJSON.result2);
}
};
// (c)
// 以下のようにresponseTypeに「json」を指定するとレスポンスのContent-Typeヘッダのcharsetの指定や
// 「overrideMimeType」が効かなくなり、レスポンスボディパース時に強制的にUTF-8が選択されます。
// xhttp.responseType = 'json';

xhttp.open('POST', this.dataset.url);
// サーバーサイドのサンプル実装にて、レスポンスのContent-Typeを制御するためにAcceptヘッダーを設定します。
xhttp.setRequestHeader('Accept', 'application/json; charset=' + responseCharset);
// IE11対策。Chrome, FirefoxはBlob生成時の「type」の指定によりContent-Typeヘッダが付与されましたが、
// IE11ではsetRequestHeaderしなければContet-Typeが付与されませんでした。
xhttp.setRequestHeader('Content-Type', mimeType);
xhttp.send(blobRequestBody);
};

document.getElementById('send_button_servlet')
.addEventListener('click', postJSON);
document.getElementById('send_button_spring')
.addEventListener('click', postJSON);

</script>
</body>
</html>


実行イメージ

↓送信ボタンを押下するとリクエストが送信され、レスポンスが返るとアラートダイアログが表示される単純な処理です(*2)。

picture1.jpg

↓リクエストボディ16進数表記「param2」の部分です。「こんにちは」が「Shift_JIS」でエンコードされています(*3)。

picture2.jpg

↓レスポンスボディ16進数表記「result2」の部分です。「ハローワールド」が「EUC-JP」でエンコードされています。

picture3.jpg


説明


課題となるXMLHttpRequestの動作~リクエスト~

以下のような素直(?)な方法では、リクエストボディは強制的にUTF-8でエンコードされてしまいます(*4)。


var requestBody = JSON.stringify({
param1: document.getElementById('param1').value,
param2: document.getElementById('param2').value
});
xhttp.setRequestHeader('Content-Type', 'application/json; charset=Shift_JIS');
xhttp.send(requestBody);

リクエストボディをUTF-8以外でエンコードするには若干の対策が必要となります。


課題となるXMLHttpRequestの動作~レスポンス~

以下のように「responseType」に「json」を指定すると、Content-Typeヘッダフィールドのcharsetの指定も、

後述する「overrideMimeType」の指定も無視され、レスポンスボディのエンコーディングは強制的に「UTF-8」と判断されます。(*5)

xhttp.responseType = 'json';

「responseType」が「text」の場合、

レスポンスのContent-Typeヘッダフィールドのcharsetからレスポンスボディのエンコーディングが判断されます。

よって、以下のようにcharsetが明示されている場合、特別な実装をせずにUTF-8以外のエンコーディングを適用することが可能です。

Content-Type: application/json;charset=EUC-JP

ただし、Content-Typeヘッダフィールドのcharsetが明示されていない場合、レスポンスボディのエンコーディングはUTF-8と判断されるため(*6)、

以下のようなContent-Typeヘッダフィールドが返される場合、対策が必要です

Content-Type: application/json


具体的な対策~リクエスト~


(a). リクエストボディのJSONは、エンコーディング用のライブラリを使用し自前でエンコードする

今回の実装では「encoding.js」というMITライセンスで提供されているライブラリを使用しています。

                // (a)

// JSON文字列を指定されたエンコーディングに変換します。
// encoding.jsに対してエンコーディングを指定する文字列は、encoding.jsのドキュメントに従います。
// https://github.com/polygonplanet/encoding.js/blob/master/README_ja.md
var charsetNameMapping = {
'Shift_JIS': 'SJIS',
'UTF-8': 'UTF8',
'EUC-JP': 'EUCJP',
};
var encodedRequestBody = Encoding.convert(requestBody, {
from: 'UNICODE',
to: charsetNameMapping[requestCharset],
type: 'arraybuffer'
});


(b). sendメソッドの引数bodyはBlob型にする

sendメソッドの引数にBlobを渡した場合は、Stringを渡した場合と異なり、「強制的にUTF-8でエンコードされる」ことがありません。

また、Blobオブジェクトの「type」属性がリクエストのContent-Typeヘッダーフィールドの値となるため任意のcharsetを指定可能です(*7)。

                // (b)

// リクエストボディとして送信するBlobを生成します。
// 送信時に、指定したcharsetがContent-Typeヘッダに付与されるよう「type」を明示します。
var mimeType = 'application/json; charset=' + requestCharset;
var blobRequestBody = new Blob([new Uint8Array(encodedRequestBody)], {type: mimeType});
// (中略)
// IE11対策。Chrome, FirefoxはBlob生成時の「type」の指定によりContent-Typeヘッダが付与されましたが、
// IE11ではsetRequestHeaderしなければContet-Typeが付与されませんでした。
xhttp.setRequestHeader('Content-Type', mimeType);
xhttp.send(blobRequestBody);


具体的な対策~レスポンス~


(c). 「responseType」は「text」のままとする

「responseType」に何も指定しないか、または、以下のように「text」を指定します。

xhttp.responseType = 'text';


(d). レスポンスのContent-Typeヘッダーフィールドにcharsetが明示されていない場合、かつ、レスポンスボディのエンコーディングがUTF-8でない場合「overrideMimeType」メソッドでcharsetを明示したContent-Typeを指定する(*8)。

以下のようにContent-Typeヘッダーフィールドにcharsetが明示されていない場合、「overrideMimeType」で明示します。

Content-Type: application/json

xhttp.overrideMimeType('application/json; charset=Shift_JIS');


注釈および参考サイト

*1 極力「UTF-8」を使用すべき旨は各ドキュメントに記載されています。

「JSON text SHALL be encoded in Unicode. The default encoding is UTF-8.」(rfc4627>「3. Encoding」2019/4/29閲覧)

「Authors are strongly encouraged to always encode their resources using UTF-8.」(WHATWG「XMLHttpRequest Living Standard 4.6.6. Response body」2019/4/29閲覧)

*2 動作確認は、Chrome(73.0.3683.103), FireFox(66.0.3), IE11(11.0.9600.19326)で実施しました。

*3 当キャプチャーはFiddlerを使用して取得しました。以下記事をFiddlerの使い方に関して参考にさせていただきました。

HTTP通信のキャプチャをとるツールFiddlerをWindowsにインストールする (2019/4/29閲覧)

*4 「XMLHttpRequest Living Standard」の以下の個所を参考に動作を確認しました。

「Set action to an action that runs UTF-8 encode on object」(WHATWG「XMLHttpRequest Living Standard 5.2. Body mixin」2019/4/29閲覧)

*5 「XMLHttpRequest Living Standard」の以下の個所を参考に動作を確認しました。

「Let jsonText be the result of running UTF-8 decode on bytes.」(WHATWG「XMLHttpRequest Living Standard 6. JSON」2019/4/30閲覧)

*6 「XMLHttpRequest Living Standard」の以下の個所を参考に動作を確認しました。

「If charset is null, then set charset to UTF-8.」(WHATWG「XMLHttpRequest Living Standard 4.6.6. Response body」2019/4/29閲覧)

*7 「XMLHttpRequest Living Standard」の以下の個所を参考に動作を確認しました。

「If object’s type attribute is not the empty byte sequence, set Content-Type to its value.」(WHATWG「XMLHttpRequest Living Standard 5.2. Body mixin」2019/4/29閲覧)

*8 当実装方法は他の方の記事でも紹介されています。

Javascriptでのエンコーディング(2019/4/29閲覧)