7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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閲覧)

7
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?