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)。
↓リクエストボディ16進数表記「param2」の部分です。「こんにちは」が「Shift_JIS」でエンコードされています(*3)。
↓レスポンスボディ16進数表記「result2」の部分です。「ハローワールド」が「EUC-JP」でエンコードされています。
説明
課題となる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閲覧)