山崎屋の技術メモ

IT業界で働く中でテクノロジーを愛するSIerのシステムエンジニア👨‍💻 | AndroidとWebアプリの二刀流🧙‍♂️ | コードの裏にあるストーリーを綴るブログ執筆者✍️ | 日々進化するデジタル世界で学び続ける探究者🚀 | #TechLover #CodeArtisan、気になること、メモしておきたいことを書いていきます。

HTTPリクエストヘッダに日本語を含めたら文字化けして困った。そして解決した話。

HTTPのリクエストヘッダに設定した日本語が文字化けしてハマりました。
事象と原因をメモしておきます。

事象

この記事で紹介している通りに Apache/Tomcat を連携し、
Apache HTTP Server をリバースプロキシとしてTomcatと連携させる - 山崎屋の技術メモ

この記事の方法で HTTP リクエストヘッダを追加しました。
Apache HTTP サーバで応答ヘッダ(レスポンスヘッダ)を追加する - 山崎屋の技術メモ
Apache HTTP サーバで要求ヘッダ(リクエストヘッダ)を追加する - 山崎屋の技術メモ
 
httpd.conf の抜粋です。文字コードはUTF-8で保存してあります。

LoadModule headers_module modules/mod_headers.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

RequestHeader set hoge "あ"

ProxyPass        / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/

 
spring bootでサーバ側の処理を記述し、ブラウザに全てのHTTPリクエストヘッダを出すようにしました。
コントローラです。

@RestController
public class SampleController {
	@GetMapping(value = "/", produces = "text/html; charset=UTF-8")
	public String index(HttpServletRequest request) throws UnsupportedEncodingException {
		Enumeration<String> headerNames = request.getHeaderNames();
		String result = "";
		while (headerNames.hasMoreElements()) {
			String headerName = headerNames.nextElement();
			String value = request.getHeader(headerName);
			result += headerName + ": " + value + "<br>" + "\r\n";
		}
		return result;
	}
}

 
Apache HTTP Server と Tomcat を起動して、ブラウザから「http://localhost」にアクセスすると、次のような結果が出ました。キー「hoge」の値は「あ」を期待していましたが、「ã□□」のような感じで文字化けしてしまいます。

調査開始

まず、デバッグとしてUTF-8で文字コードを出力すると「E3 81 82」のはずが「C3 A3 C2 81 C2 82」となっていました。
ググっても答えは出てこないですが、なんやら「二重エンコード」なるキーワードが出てきました。
encode encode - private tips
 
なんか、今回作ったプログラムは文字コードについて考慮が足りない気がしてきました。
httpd.confに 書かれている「あ」は UTF-8 です。コード値は「E3 81 82」です。
そして Java は内部的に UTF-16 で持っています。コード値は「3042」です。
 
つまり Tomcat 側で「E3 81 82」→ 「3042」の変換をする必要があるのですが、私は「httpd.conf は UTF-8 で書かれているよ」とTomcatに伝えていません。
 
私の仮説です。
Java が String の中で「E3 81 82」のまま持っていて、これを、UTF-16 の文字コードと勘違いしています。
ブラウザに返す際、UTF-16 → UTF-8 に再度変換しており、この再度変換したコード値が「C3 A3 C2 81 C2 82」となってしまいます。

実際の UTF-16 → UTF-8 変換ルールで検証したいですが、調べたら難しそうなのでまだチャレンジしていません。やる気出たときに調べて報告します。

解決

「request.getHeader(xxxx)」で文字列を得る際、Java はおそらくその文字列が UTF-16 という前提で動いています。
実際は UTF-8 なのでひと工夫します。
 
いったん、「E3 81 82」のまま String 変数に格納し、それをそのままバイト配列にします。
new String(xxxx,xxxx) を使って、このバイト配列が UTF-8 だよと教えてから、改めて文字列変数に格納してあげれば、UTF-8 → UTF-16 変換作業を行ったのち String 変数に格納してくれます。

@RestController
public class SampleController {
	@GetMapping(value = "/", produces = "text/html; charset=UTF-8")
	public String index(HttpServletRequest request) throws UnsupportedEncodingException {
		Enumeration<String> headerNames = request.getHeaderNames();
		String result = "";
		while (headerNames.hasMoreElements()) {
			String headerName = headerNames.nextElement();
			String value = new String(request.getHeader(headerName).getBytes(StandardCharsets.ISO_8859_1),
					StandardCharsets.UTF_8);
			result += headerName + ": " + value + "<br>" + "\r\n";
		}
		return result;
	}
}

「request.getHeader(headerName).getBytes(StandardCharsets.ISO_8859_1)」で「ISO_8859_1」という文字コードを指定しています。
この文字コードは 1 バイト文字のみ定義されており、余計な変換がされません。String 変数からバイト配列をそのまま取得したい時に使用します。(余計な文字コード変換をして欲しくないときに使います。)
 

まとめ

request.getHeader メソッドは文字コードを意識するような引数を求めていません。(そもそも HTTP ヘッダには全角文字の扱いが定義されていません。)
HTTPリクエストヘッダに日本語を含める場合は注意しましょう。