LoginSignup
0

More than 5 years have passed since last update.

XmlSerializerの使い方を誤って、stringにゴミ(BOM)が混入した

Posted at

前提

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");

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
0