3アプリの運用で実際に踏んだ落とし穴のカタログである。各項目は「症状 → 原因 → 対策(実装済みの守り)」で記す。守りはパッケージに焼き込んであるため、SDK / factory を通っている限り再発しない。自前コードで迂回した箇所だけが再発点になる。
1. env の空白 → 401
- 症状: 正しいはずの API キーで 4S が 401 を返す。目視では env は正しい
- 原因: env 値の前後空白・末尾スラッシュ(コピペ時の混入)が Authorization ヘッダ / URL を壊す
- 対策:
createFoursConfigが全値をtrim()、base URL はnormalizeBase()で末尾スラッシュも除去。isCronAuthorizedも trim 硬化済み。SDK を通らない手書き fetch だけが危険
2. Blob eventual consistency
- 症状: 連続 POST の直後に
list()すると直前に書いたデータが見えない / 消えたように見える - 原因: Vercel Blob の
list()は eventual consistent - 対策:
@event/asset-kitは存在確認を byte-hash キー(rehostByHash— 同一バイト列は同一 URL)に寄せ、list()に依存しない。読み戻し検証が必要な書き込みは書いた URL を直接 GET する
3. 凍結バンドル hash → 保存が永久 409
- 症状: デプロイ後、admin の保存が全て conflict(409)になり、リロードしても直らない
- 原因: 本番の fs はデプロイ時点で凍結されたバンドル。fs 基準の hash と GitHub HEAD の hash が最初の commit 以降ずっと乖離する
- 対策:
createContentStore.readResourceWithHashは本番では GitHub 最新を読む。さらに書き込み成功時は「今書いた内容」の sha256 を返し、次の保存の基準を確実に一致させる(第14章の不変条件)
4. dev の distDir 共有事故
- 症状:
next dev起動中にyarn buildを打つと dev が壊れる(謎の 500 / モジュール解決エラー) - 原因: dev と build が同じ
.nextを共有し、build が dev のキャッシュ/マニフェストを上書きする - 対策:
createNextConfigがNEXT_DIST_DIR/BUILD_LOCAL=1で distDir を.next-localに分離する。dev 中の検証は typecheck かBUILD_LOCAL=1ビルドを使う
5. .next cache 破損 → 全 route 500
- 症状: dev で突然すべての route が 500。
.next/cacheを消すと直る - 原因: dev の webpack ファイルキャッシュ破損
- 対策:
createNextConfigの webpack hook が dev ではcfg.cache = { type: "memory" }を強制。破損しうるファイルキャッシュ自体を持たない
6. Upstash の JSON 自動デシリアライズ
- 症状: KV に保存した JSON 文字列を
JSON.parseすると本番だけ"[object Object]"で落ちる。dev(メモリ shim)では再現しない - 原因: Upstash は値が JSON らしいと get / hget / hgetall で自動デシリアライズしたオブジェクトを返す
- 対策:
@event/coreのRealKvは string 以外をJSON.stringifyし直して返す coercion ガードを全 getter に内蔵。KvLikeを通す限り「保存時の文字列」が必ず戻る
7. transform:scale プレビューの直撮りズレ
- 症状: 書き出した PNG で文字が数 px ズレる / にじむ。画面では正常
- 原因: 縮小プレビュー(transform: scale)を直接 rasterize するとサブピクセル丸めが焼き込まれる
- 対策:
captureCanvasは画面外に native 寸法・transform: none で mount してから撮る。captureElementCanvasも clone にtransform: "none"を強制する
8. serverless コールドスタートで画像が抜ける
- 症状: 一括書き出しで本番でだけロゴ・顔写真が白抜けになる。1枚ずつなら成功する
- 原因: 大量ジョブが img-proxy / 画像ホストへ同時 fetch し、serverless のコールドスタートに殺到してタイムアウトする
- 対策:
inlineImagesは併走上限6 + 失敗リトライ3回(バックオフ付き)で data URI 化し、waitImagesで load 完了を待ってから rasterize する。bulkZip側も worker pool(既定4)で総量を絞る
9. edge bundle への node:* 混入
- 症状: middleware のビルドが
node:fs/node:cryptoを解決できず失敗する、または edge サイズ超過 - 原因: middleware(edge runtime)から
@event/coreの barrel を import すると、contentStore(node:fs / node:crypto / octokit)まで束で edge bundle に入る - 対策: edge から使うものは subpath から import する —
@event/core/auth(constantTimeEqual)のみ。@event/shellのcreateAuthGateMiddlewareは最初からそう書かれている。同型の対策として@event/session-card/persist(server 専用)と@event/asset-kitの 4 entrypoint 分割がある
10. 楽観ロックの 409 は「正常系」
- 症状: 編集保存で「他のセッションが先に更新しました」
- 原因: 本物の競合。別タブ / 別担当者が先に保存した
- 対策: 仕様どおりの動作である。リロードして最新 hash を取得してから再編集する。カード overlay(
OverlayStore.patch)は single-record PATCH + 自動リトライ6回なので、同一セッションを同時編集しない限りユーザーには見えない