💊

副作用に苦しめられた件

に公開

最近、作っているアプリのCSV取込機能で以下のような不具合が見つかりました。

  • レコードが100件を超えるとエラーで終了する
  • ちょうど100件目を境に行が結合してしまう

しかし、コードのどこを見ても「100件で分割する」ような処理は見当たらず、バグは迷宮入りしそうでした。

🔍 原因調査:BOM判定メソッドが怪しい

デバッグしてみると、ファイルにBOMがあろうとなかろうと、HasBomメソッドが常にfalseを返していることが判明しました。

`using (var reader = new StreamReader(ms, ConfigUserImportEncode))
{
    if (HasBom(reader))
    {
        reader.BaseStream.Seek(3, SeekOrigin.Begin);
    }
    *// CSVパース処理が続く...*
}`

一見正しそうに見えるBOM判定メソッド:

*/// <summary>/// BOMの有無を確認/// </summary>*
static bool HasBom(StreamReader reader)
{
    reader.Peek(); 
    if (reader.CurrentEncoding.Equals(Encoding.UTF8) && reader.BaseStream.Length >= 3)
    {
        byte[] bom = new byte[3];
        reader.BaseStream.Read(bom, 0, 3);  *// ⚠️ ここが問題!*

        return bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF;
    }

    return false;
}

💡 問題の本質:StreamReaderの内部バッファ

このメソッドを削除すると、なぜか正常に動作することに気づきました。

実は、StreamReaderはデフォルトで自動的にBOMを処理してくれるのです!

AIに相談したところ、BaseStream.Read()でストリームの読み込み位置が進んでしまうことが原因ではないか、という回答が。公式ドキュメントを確認すると、まさにその通りでした。

何が起きていたか

  1. reader.Peek()でStreamReaderが内部バッファにデータを読み込む(デフォルトバッファサイズ:1024バイト)
  2. BaseStream.Read()で3バイト読み込む → ストリーム位置が進む
  3. StreamReaderの内部バッファとストリーム位置に不整合が発生
  4. 約100件(バッファサイズに依存)を超えたところで、バッファの境界で読み込みがおかしくなる

📝 教訓

これじゃHasBomじゃなくて、HasBombですね!

・・・・

・・・・

・・・・

このバグから

  • 副作用への考慮
  • バッファリングの仕組みを理解することの重要性
  • 公式ドキュメントを読むことの重要性

を学びました

余談ですが、BOMの歴史を見ると、エンコーディング周りの苦しみが感じられますね。

Discussion