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

Ajaxでの「Content-Type: application/json」の処理において、UTF-8以外のエンコーディングを使用することに予想外に苦労したため、調査・実装した内容をまとめようと思います。  
UTF-8以外のエンコーディングの使用はかなり稀、かつ、極力避けるべきと思います(*1)が、
古いShift_JIS主体のシステムと連携しなければならない場合など、
当記事の内容が役立つこともあるかもしれません。  

当記事ではクライアントサイド(javascript)について紹介し、
サーバーサイド(java Servlet&SpringMVC)については別の記事で紹介します。

#デモ用ソースコード
実装のポイントは以下の通りです。ソースコードは[GitHub](https://github.com/st-user/JISAjaxJSONSample/blob/master/src/main/webapp/index.html)にもアップロードしています。

(a). リクエストボディのJSONは、エンコーディング用のライブラリを使用し自前でエンコードする
(b). sendメソッドの引数bodyはBlob型にする
(c). 「responseType」は「text」のままとする
(d). レスポンスのContent-Typeヘッダーフィールドにcharsetが明示されていない場合、かつ、レスポンスボディのエンコーディングがUTF-8でない場合「overrideMimeType」メソッドでcharsetを明示したContent-Typeを指定する

```html
<!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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/338425/2ed610e4-93d2-0792-68a4-5880fc927b6a.jpeg)

↓リクエストボディ16進数表記「param2」の部分です。「こんにちは」が「Shift_JIS」でエンコードされています(*3)。
![picture2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/338425/78d14efb-6ac7-ff60-afbd-a6ed8809c4c1.jpeg)
↓レスポンスボディ16進数表記「result2」の部分です。「ハローワールド」が「EUC-JP」でエンコードされています。
![picture3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/338425/202ac4eb-a21a-e141-e5b7-acc9f54b36c9.jpeg)

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

```js

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)

``` js
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](https://github.com/polygonplanet/encoding.js/blob/master/README_ja.md)」というMITライセンスで提供されているライブラリを使用しています。

```js
				// (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)。

``` js
				// (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」を指定します。

```js
xhttp.responseType = 'text';
```

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

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

``` 
Content-Type: application/json
```

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

# 注釈および参考サイト
*1 極力「UTF-8」を使用すべき旨は各ドキュメントに記載されています。
「JSON text SHALL be encoded in Unicode.  The default encoding is UTF-8.」([rfc4627](https://www.ietf.org/rfc/rfc4627.txt)>「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」](https://xhr.spec.whatwg.org/#text-response)2019/4/29閲覧)

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

*3 当キャプチャーは[Fiddler](https://www.telerik.com/fiddler)を使用して取得しました。以下記事をFiddlerの使い方に関して参考にさせていただきました。
[HTTP通信のキャプチャをとるツールFiddlerをWindowsにインストールする](https://qiita.com/taketakekaho/items/397bc6e9afa32329edd0) (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」](https://fetch.spec.whatwg.org/#concept-bodyinit-extract)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」](https://infra.spec.whatwg.org/#parse-json-from-bytes)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」](https://xhr.spec.whatwg.org/#text-response)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」](https://fetch.spec.whatwg.org/#concept-bodyinit-extract)2019/4/29閲覧)

*8 当実装方法は他の方の記事でも紹介されています。
[Javascriptでのエンコーディング](https://qiita.com/nbjiao/items/9be484793ff545bace32)(2019/4/29閲覧)