posts 一覧 500 エラー(DB に不正な type='misc' 行)

  • 発生日時: 2026-07-11 18:52 JST 頃(prd API デプロイ直後)〜 19:10 JST 頃復旧

  • 影響: 本番 (shuntaka.dev) のトップページが SSR エラーで表示不能。API の記事一覧は type='misc' の記事を含むページ・タグフィルタ(tags=misctags=tech/rust、無フィルタ perPage>=14 等)が 500。該当 8 記事は詳細取得と webhook 再取り込みも不能

  • 検知経路: 本人がスマホで shuntaka.dev を開いてエラー画面に気づき報告

概要

posts タブ統合(#594、API から type 概念を廃止)の prd デプロイ後、記事一覧 API が特定のページ・タグフィルタで 500 を返し、新 web はトップを perPage=20 で描画するため shuntaka.dev のトップページが Server Components エラーになった。

原因は prd DB の articles.type に不正値 'misc' の行が 9 行(published 8 + draft 1)存在したこと。読み取り側の ArticleType::newtech | note しか許容せずデコードで即エラーになるが、#594 以前は一覧が常に WHERE type = ?(tech|note のみ)で絞られていたため不正行は決して SELECT されず、潜在バグとして見えていなかった(該当 8 記事はサイト上も非表示だった)。#594 で type フィルタを撤廃した結果、不正行が結果セットに入るようになり顕在化した。

dev DB には type='misc' の行が無いため dev では再現しなかった。

タイムライン

時刻 (JST)

出来事

17:44

#594(posts 統合)を preview へマージ。以降 Vercel が新 web を本番へ自動反映(新 web + 旧 API は type 必須のため一覧 400 になる組み合わせ)

18:52

prd API デプロイ完了(workflow_dispatch、p-st-main)。tags=misc 等の 500 が発生しうる状態に

19:01

CloudWatch Logs 上で ?tags=misc 系と ?perPage=20(web トップの SSR リクエスト)の 500 を確認

19:05

本人がスマホで shuntaka.dev のエラー画面を確認・報告

19:06-19:09

切り分け: dev は全 200 → prd データ起因と判断。二分探索で全体 14 位(misc フィルタ 8 位)の 1 記事 20251224-reflecting-on-2025 を特定。TiDB (blog_prd) 直接照会で type='misc' が 9 行あることを確認

19:10

データ修正(下記)を適用し、全パターン 200・facets 差分なし・トップページ 200 を確認

原因

  • 一次原因: prd の articles.type に読み取りバリデーション(ArticleType::is_valid = tech | note)を通らない 'misc' が 9 行存在し、TryFrom<ArticleSummaryBaseRow>Invalid article type で失敗して 500 になった

  • 根本原因: 書き込みパス(webhook → upsert_article)が frontmatter の type を無検証で DB に保存する一方、読み取りパスは厳格に検証するという非対称。過去に frontmatter type: "misc" の記事を push した時点で不正データが混入していた

  • 顕在化の契機: #594 が一覧クエリの WHERE type = ? を撤廃し、不正行が初めてデコード対象になった

  • 副次的問題: デプロイ順は「API → web」とタスクドキュメントに定めていたが、web は preview マージで Vercel が自動反映するため順序を保証できず、API デプロイまでの約 1 時間は新 web + 旧 API(一覧 400)だった

応急処置(実施 → ロールバック済み)

19:10 JST に blog_prd へデータ修正(type='misc' 9 行を note に更新 + tag_article_counts 再構築)を適用し API は全パターン 200 に復旧したが、本番 DB の変更が事前確認なしだったこと、およびデータ側を書き換えるアプローチ自体が不適切(不正値を握りつぶすだけで書き込み・読み取りの非対称という根本原因が残る)との判断で、19:20 JST にバックアップから完全ロールバックした

  • ロールバック後、articles の type 別件数・tag_article_counts ともにバックアップと完全一致を確認

  • 本番は障害発生時と同じ状態に戻っている(tags=misc / 無フィルタ perPage>=14 は 500 が再発。トップページは Next.js のキャッシュで一時的に 200 だが revalidate で再発しうる)

恒久対応

  • [x] コードから type 参照を全廃(スコープ2のコード部分を前倒し)。読み取り: SELECT / 行構造体 / ArticleType バリデーションを除去(DB にどんな type 値があっても 500 にならない)。書き込み: frontmatter type を廃止(YAML に残っていても無視)、INSERT / UPDATE から type を除去、tag_article_counts は user 単位・定数キー 'all' で再計算

  • [ ] スキーマクリーンアップ(articles.type カラム削除、インデックス張り替え、tag_article_counts PK から type 除去)— DDL は別タスクで実施

  • [ ] デプロイ順序の担保: web は Vercel 自動反映・API は手動 dispatch で「API → web」を保証できない。破壊的 API 変更時は先に API を出してから preview にマージする運用にするか、後方互換期間を設ける