プログラマー探偵の事件簿:「invalid character '\x00' after top-level value」の謎を12年前のつぶやきが解く
随分昔のつぶやきが不可解なエラーの謎を説いた話である。
事件の始まり
TWSNMP FCでWebサーバーのメトリックを取得するポーリングを開発していた時のことである。Apcheやnginxのメトリックは簡単に取得できるようになったが、Fiberの
から取得するJSON形式のメトリック
{"pid":{ "cpu":0.4568381746582226, "ram":20516864, "conns":3 },
"os": { "cpu":8.759124087593099, "ram":3997155328, "conns":44,
"total_ram":8245489664, "load_avg":0.51 }}
を処理した時に
「invalid character '\x00' after top-level value」
という謎のエラーに遭遇した。
ログに出力しても問題ない
Webサーバーから受信した応答をログに出力してみたが、何の問題もない。
エディターにコピーしてJSONのエラーを確認してみたが、こちらも問題はない。前後のスペースを取り除く処理を入れてみたが効果がなかった。
12年前のつぶやきがヒントになる
Googleさんに聞いてみるといろいろあったが
12年前のつぶやきが見つかった。
ソースコードを見直して怪しいところを発見
Webサーバーからの応答を受信する処理を見直すと
body := make([]byte, 64*1024)
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return "", "", 0, err
}
defer resp.Body.Close()
_, err = resp.Body.Read(body)
if err == io.EOF {
err = nil
}
return resp.Status, string(body), resp.StatusCode, err
のように受信する応答用のバッファを
body := make([]byte, 64*1024)
でサイズを指定して確保している。これが怪しい。
これをstring(body) で文字列にしているが受信した応答のうしろに0x00が続いているようだ。どうやら、はっきりバイト数分のスライスにならないので怒られているようだ。
受信方法を変えて解決
応答を受信する処理を
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return "", "", 0, err
}
defer resp.Body.Close()
// メモリー不足をおこさないための64MBまで
if resp.ContentLength > 1024*1024*64 {
return "", "", 0, fmt.Errorf("http rest seize over len=%d", resp.ContentLength)
}
if body, err := io.ReadAll(resp.Body); err == nil {
return resp.Status, string(body), resp.StatusCode, err
} else {
if err == io.EOF {
err = nil
}
return resp.Status, "", resp.StatusCode, err
}
のように変えると謎のエラーは解決した。
元々の処理は、応答が大きすぎてメモリーを圧迫しないために受信するデータを制限する処理であった。修正後は、応答のサイズを先にチェックして、64MB以下なら読み込むことにした。読み込む時にio.ReadAllを使うことで
はっきりバイト数分のスライスにできたようである。
ここで助手の猫が天から一言
「同じ文字列(string)でも見えない部分に違いがある」
とのこと!
12年前のつぶやきに感謝!
私のつぶやきも12年後に誰かの役に立つかも?
開発のための諸経費(機材、Appleの開発者、サーバー運用)に利用します。 ソフトウェアのマニュアルをnoteの記事で提供しています。 サポートによりnoteの運営にも貢献できるのでよろしくお願います。