EmDash の publish で CONTENT_PUBLISH_ERROR が出た原因と解決方法

EmDash CMS でページを更新して publish しようとしたところ、管理画面で次のエラーが発生しました。調査の結果Cloudflare D1 上の FTS5 検索インデックスが破損していたことが原因と判明した。

EmDash CMS でページを更新して publish しようとしたところ、APIが以下のレスポンスを返していました。

{
  "error": {
    "code": "CONTENT_PUBLISH_ERROR",
    "message": "Failed to publish content"
  }
}

この時のCloudflare Workers のログを確認すると、次のエラーが確認できました。

Content publish error:
Error: D1_ERROR: database disk image is malformed: SQLITE_CORRUPT
(extended: SQLITE_CORRUPT_VTAB)

原因

原因は、Cloudflare D1 上にある EmDash の検索用 FTS5 仮想テーブルが破損していたことでした。

EmDash では、検索対象の collection に対して SQLite の FTS5 index が作成されます。今回のサイトでは pages collection にも search support を付けていたためec_pages の更新時に FTS trigger が動く構成になっていました。

publish 処理では、下書き revision の内容を実テーブル ec_pages に反映します。その更新時に _emdash_fts_pages_update trigger が走り、破損していた FTS5 仮想テーブルに触れたことで SQLITE_CORRUPT_VTAB が発生していました。

つまり、publish そのもののバリデーションエラーではなく、publish に伴う検索インデックス更新で落ちていました。

調査したこと

まず、publish API の実装を確認しました。EmDash の publish 処理は大まかに次の流れです。

1. 対象 content を取得

2. 権限チェック

3. draft revision を live revision に昇格

4. revision の内容を content table に同期

5. status を published に更新

6. cache を invalidate

リモート D1 の schema を確認するとpages 用の FTS table と trigger が存在していました。

emdashfts_pages
emdashfts_pages_insert
emdashfts_pages_update
emdashfts_pages_delete

このためpages の FTS table / trigger が publish 失敗の原因だと判断しました。

解決方法

今回は、サイトの検索を記事検索用途に絞る方針にしたためpages を検索対象から外しました。

まず、リモート D1 で壊れていた pages の FTS table / trigger を削除しました。

DROP TRIGGER IF EXISTS emdashfts_pages_insert;
DROP TRIGGER IF EXISTS emdashfts_pages_update;
DROP TRIGGER IF EXISTS emdashfts_pages_delete;
DROP TABLE IF EXISTS emdashfts_pages;

その後 pages collection の検索設定も無効化しました。

UPDATE emdashcollections
SET search_config = '{"enabled":false}'
WHERE slug = 'pages';

コード側の再発防止

DB だけ直しても、seed 側で pagessearch support が残っていると、今後また検索対象に戻る可能性があります。

そのためseed/seed.json も変更しました。

変更前:

{
  "slug": "pages",
  "label": "Pages",
  "labelSingular": "Page",
  "supports": ["drafts", "revisions", "search"],
  "fields": [
    {
      "slug": "title",
      "label": "Title",
      "type": "string",
      "required": true,
      "searchable": true
    },
    {
      "slug": "content",
      "label": "Content",
      "type": "portableText",
      "searchable": true
    }
  ]
}

変更後:

{
  "slug": "pages",
  "label": "Pages",
  "labelSingular": "Page",
  "supports": ["drafts", "revisions"],
  "fields": [
    {
      "slug": "title",
      "label": "Title",
      "type": "string",
      "required": true
    },
    {
      "slug": "content",
      "label": "Content",
      "type": "portableText"
    }
  ]
}

まとめ

今回の CONTENT_PUBLISH_ERROR は、表面的には EmDash の publish 失敗でしたが、実際には Cloudflare D1 上の FTS5 検索インデックス破損が原因でした。

ポイントは次の通りです。

  • CONTENT_PUBLISH_ERROR だけでは原因は分からない
  • Cloudflare Workers のログを見ると SQLITE_CORRUPT_VTAB が出ていた
  • publish 時に ec_pages が更新され、FTS trigger が動いていた
  • 壊れていた _emdash_fts_pages を削除して解消した
  • このサイトでは pages 検索が不要だったためpages から search support を外した
  • 検索対象は posts に限定した
  • D1 transaction fallback の互換性も patch-package で補正した

EmDash で publish が失敗した場合は、まず管理画面のエラーだけで判断せず、Cloudflare Workers の実ログを見るのが重要です。特に SQLITE_CORRUPT_VTAB が出ている場合は、content 本体ではなく FTS5 の仮想テーブルや trigger を疑うと切り分けが早くなります。

3 Comments

  1. John
    test test test
  2. abc
    aaa
  3. abc
    abc