前提
UTF-8の先頭にBOM 3バイト(0xEFBBBF)が付くこと自体は仕様に存在するが、
HTTPレスポンスの場合、ヘッダに文字コードは書かれているため、BOMが付いていると問題になることが多い
経緯
ASP.NET Web APIを使ってXMLのレスポンスを返すAPIを作成していたところ、レスポンスのボディにBOMが混入し、受け手のシステムが処理できないという問題が起きました。
最初はStringContentを疑ったが、それは間違いだった
問題のソースでは、StringContentを使ってレスポンスのBODYを指定していました。
// responseStringはstring
var content = new StringContent(responseString, Encoding.UTF8, "application/xml");
テキストファイルを扱っていたときの知識から、第2引数が悪さをしているのではないかと思い、次のように書き換えてみました。
var content = new StringContent(responseString, new UTF8Encoding(false), "application/xml");
ところが、デバッグしてみると変更前後のどちらのレスポンスにもBOMが混入していました。
問題はその前のXmlSerializer
色々調べてみると、実はresponseStringの中身に問題があることが判明しました。
responseStringには下記のようなメソッドを使って、エンティティをXML文字列にシリアライズした値を代入していました。
public static string Serialize<T>(Object obj, bool isEmptyNamespace = true) where T : class
{
using (MemoryStream stream = new MemoryStream())
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
// ヘッダに文字コードを追加する
XmlWriterSettings settings = new XmlWriterSettings();
settings.Encoding = Encoding.UTF8;
using (XmlWriter writer = XmlWriter.Create(stream, settings))
{
if (isEmptyNamespace)
{
var xmlNamespace = new XmlSerializerNamespaces();
// XMLの名前空間を空白にする
xmlNamespace.Add(string.Empty, string.Empty);
xmlSerializer.Serialize(writer, obj as T, xmlNamespace);
}
else
{
xmlSerializer.Serialize(writer, obj);
}
}
// ストリームを開始位置に戻す
stream.Position = 0;
return Encoding.UTF8.GetString(stream.ToArray());
}
}
問題になるのはXmlWriterSettingsの部分です。ここで文字コードをUTF8にセットしていますが、この状態だと MemoryStreamにはBOMも書き込まれてしまう のです。
結果、stream.ToArray()したとき、GetString()にはBOM 3バイトを含んだバイト配列が渡されます。
string型に変換されたあと、クイックウォッチで見てもBOMは表示されないため、一見正常な文字列になっているように見えます。しかし、実際には3バイトのゴミがstring型の先頭に混入した状態となります。
これが巡り巡って、レスポンスのボディに乗ってきてしまい、不具合の原因となっていました。
直接的な解決:XMLのシリアライズ・メソッドを修正
XmlWriterSettingsで文字コードをBOMなしUTF8と指定すれば、ひとまず解決します。
public static string Serialize<T>(Object obj, bool isEmptyNamespace = true) where T : class
{
using (MemoryStream stream = new MemoryStream())
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
// ヘッダに文字コードを追加する
XmlWriterSettings settings = new XmlWriterSettings();
settings.Encoding = new UTF8Encoding(false);
using (XmlWriter writer = XmlWriter.Create(stream, settings))
{
if (isEmptyNamespace)
{
var xmlNamespace = new XmlSerializerNamespaces();
// XMLの名前空間を空白にする
xmlNamespace.Add(string.Empty, string.Empty);
xmlSerializer.Serialize(writer, obj as T, xmlNamespace);
}
else
{
xmlSerializer.Serialize(writer, obj);
}
}
// ストリームを開始位置に戻す
stream.Position = 0;
return new UTF8Encoding(false).GetString(stream.ToArray());
}
}
そもそもの話、わざわざXmlSerializerを使う必要はない
せっかくWeb API使ってるんで、StringContentとか介さずに、こうしてれば問題は起きなかったですよね。
// responseはXMLへシリアライズしたいモデル
Request.CreateResponse(HttpStatusCode.OK, response, "application/xml");