# トラブルシューティング

> Calendarpit の起動、OAuth、Calendar API、同期、watch、管理 API でよく起きるエラーの原因と対処をまとめています。

リクエストが失敗すると、Calendarpit は明示的なステータスコードとエラーメッセージを返します。未対応のフィールドやパラメータを黙って無視することはありません。

原因を調べるときは、管理画面の **API history**（`http://127.0.0.1:4173/history`）を開きます。

- 直近 200 件の Calendar API と OAuth のやり取りを確認できます。
- 各エントリには HTTP メソッド、パス、ステータスコード、ヘッダー、マスク済みのリクエストボディとレスポンスボディが記録されます。
- **Errors** トグルを使うと、ステータスが `400` 以上のレスポンスだけを表示できます。

このページの各項目は、同じ形で書かれています。正確な**エラー**出力（ある場合）、考えられる**原因**、**対処**の順です。

## エラーレスポンスの形式

### Google API のエラー形式

Calendar API のルート（`/calendar/v3/*`）、`/userinfo`、管理 API のルート（`/__calendarpit/*`）は、Google API と同じ形式でエラーを返します。

```json
{
  "error": {
    "code": 404,
    "message": "Event not found.",
    "errors": [
      {
        "domain": "calendarpit",
        "reason": "notFound",
        "message": "Event not found."
      }
    ]
  }
}
```

- トップレベルのエラーでは、`domain` は常に `"calendarpit"` です。`freebusy.query` レスポンスの中のカレンダーごとのエラーは `"domain": "global"` になります。
- `401` レスポンスには `WWW-Authenticate: Bearer` ヘッダーが付きます。
- より具体的な reason がない `400` の検証エラーは `"reason": "invalidParameter"` になります。

### OAuth のエラー形式

OAuth のルート（`/oauth/*` と `/oauth2/*`）は OAuth のエラー形式を返します。ほとんどのエラーは HTTP `400` ですが、次は例外です。

- 未実装のパスは `501` を返します。
- 保留中の同意リクエストが多すぎる場合は `429` を返します。
- 別オリジンから同意画面を送信した場合は `403` を返します。

```json
{
  "error": "invalid_grant",
  "error_description": "Invalid, expired, or mismatched authorization code."
}
```

`GET /oauth/authorize` の検証エラーは、HTTP `400` の JSON としてブラウザに直接返ります。Calendarpit は不正な認可リクエストを `redirect_uri` にリダイレクトしません。

## Calendarpit の起動

CLI で起動に失敗すると、`stderr` に `Calendarpit: <message>` を出力し、終了コード `1` で終了します。Docker の出力は `docker compose logs calendarpit` で確認できます。

### ポートが使用中

- **エラー**: `Calendarpit: Failed to start server. Is port 4173 in use?`
- **原因**: 別のプロセスがポート `4173` を使っています。
- **対処**: 別のプロセスを停止するか、`--port <n>` でポートを変更します。`--port 0` の場合は、実際の `Admin` URL を確認します。Docker ではホスト側を `127.0.0.1:4174:4173` などに変更し、ポート `4174` にアクセスします。

### データベースが使用中

- **エラー**: `Calendarpit: Database is already in use: <path>. Use a separate path or :memory:.`
- **原因**: 別のプロセスが同じ SQLite ファイルをロックしています。
- **対処**: 別のプロセスを停止するか、プロセスごとに異なる `--database` のパスまたは `:memory:` を使います。[ストレージと再起動](https://calendarpit.dev/ja/storage.md)を参照してください。

### Fixture ファイルが見つからない

- **エラー**: `Calendarpit: Fixture file not found: <path>. …`
- **原因**: Fixture のパスが作業ディレクトリ内、またはコンテナ内に存在しません。
- **対処**: 正しい絶対パスを指定します。Docker ではファイルをマウントし、コンテナ内のパスを渡します。[Fixture](https://calendarpit.dev/ja/fixtures.md)を参照してください。

### Fixture のパスがディレクトリになっている

- **エラー**: `Calendarpit: Fixture path is a directory, not a file: <path>`
- **原因**: コンテナで、バインドマウントの元パスがコンテナランタイムの VM と共有されていないため、Docker がマウント先に空のディレクトリを作成しました。Colima でホームディレクトリ外のパスを指定すると発生します。ホスト上に存在しないパスを指定した場合も同じ結果になります。
- **対処**: ホストのディレクトリをランタイムと共有する（Colima では `colima.yaml` の `mounts` に追加して再起動する）か、起動前に `docker cp` でファイルをコンテナにコピーします。[ホストのディレクトリをコンテナランタイムと共有する](https://calendarpit.dev/ja/fixtures.md#ホストのディレクトリをコンテナランタイムと共有する)を参照してください。

### Fixture ファイルを読み取れない・JSON として不正

- **エラー**: `Calendarpit: Fixture file is not readable: <path>. …` または `Calendarpit: Fixture file is not valid JSON: <path> (<reason>)`
- **原因**: コンテナの非 root ユーザーがファイルを読み取れないか、ファイルが正しい JSON ではありません。
- **対処**: ほかのユーザーも読み取れるようにする（例: モード `644`）か、JSON の構文を修正します。

### Fixture の検証に失敗する

- **エラー**: `Calendarpit: Each user with calendars must have exactly one primary calendar.`
- **原因**: 起動時 Fixture の検証に失敗しました。
- **対処**: カレンダーを持つ各ユーザーに、`"primary": true` のカレンダーを必ず 1 つ設定します。[Fixture のルール](https://calendarpit.dev/ja/fixtures.md)を参照してください。

### 再起動しても Fixture の変更が反映されない

- **原因**: 起動時 Fixture は新しいデータベースだけに適用されます。既存のデータとリセット基準は保持されます。
- **対処**:
  - 現在のデータを置き換えるには、**Settings → Test data** または `POST /__calendarpit/fixtures` で読み込みます。
  - リセット基準も変えるには、新しいデータベースのパスか `:memory:` で起動します。

### 起動方法を変えたらデータが見えない

- **原因**: 次のいずれかです。
  - 作業ディレクトリが変わった。
  - 別のデータベースのパスやボリュームを使った。
  - 前回は `:memory:` を使っていた。
- **対処**: 起動時の `Data <path>` と **Settings → Test data** を確認します。同じ絶対パスまたは Docker ボリュームを使ってください。

### Docker でブラウザ、バックエンド、Webhook の受信先に接続できない

- **原因**: ループバックはリクエスト元のコンテナ自身を指します。また、ブラウザは Compose のサービス名を解決できません。
- **対処**: ブラウザではホスト側に公開した URL、コンテナ間ではサービス名を使います。Webhook の受信先には `CALENDARPIT_WATCH_HOSTS` も設定します。[コンテナの接続先](https://calendarpit.dev/ja/installation.md)を参照してください。

### `CALENDARPIT_*` の値が反映されない

- **原因**: CLI フラグで上書きしているか、ホストで設定した環境変数を Docker に渡していません。
- **対処**: 上書きするフラグを外すか、`docker run -e` または Compose の `environment` で渡します。コンテナのポートを変更する場合は、ポートのマッピングとヘルスチェック用の `CALENDARPIT_PORT` も揃えてください。

### オプションの値が不正、未知のオプションやコマンド

- **原因**: 不正なフラグ、空の値、未対応のサブコマンドを指定しています。
- **対処**: `calendarpit --help` で確認します。サブコマンドは `serve` のみです。ポートは `0` から `65535` の整数で指定します。

## OAuth

### 本物の Google の同意画面が表示される

- **原因**: アプリの認可 URL がまだ Google（`accounts.google.com`）を指しています。
- **対処**: 認可 URL を `http://127.0.0.1:4173/oauth/authorize` に設定します。詳しくは[アプリを接続する](https://calendarpit.dev/ja/connect.md)を参照してください。

### トークンの交換や更新で Google に接続してしまう

- **原因**: OAuth クライアントがまだ Google のトークンエンドポイントを使っています。Calendar API のエンドポイントを設定しても、OAuth の呼び出し先は変わりません。
- **対処**: トークン URL を `http://127.0.0.1:4173/oauth/token` に上書きします。`/oauth/revoke` と `/userinfo` を使う場合は、それらも上書きします。詳しくは[ローカル OAuth](https://calendarpit.dev/ja/oauth.md)を参照してください。

### Google のデフォルトの OAuth パスで `501` になる

- **エラー**: `/oauth2/...` や `/oauth/tokeninfo` で `501 unsupportedFeature`
- **原因**: SDK が Google のデフォルトの OAuth パス（`/oauth2/v4/token`、`/oauth2/v3/userinfo`、`/oauth/tokeninfo`）を呼んでいます。
- **対処**: エンドポイントを `/oauth/authorize`、`/oauth/token`、`/oauth/revoke`、`/userinfo` に明示的に上書きします。

### 登録されていないリダイレクト URI

- **エラー**: `invalid_request`: `Unregistered redirect URI.`
- **原因**: `redirect_uri` が `clients[].redirectUris` のどのエントリとも完全には一致していません。
  - `localhost` と `127.0.0.1` の違い、ポート、パス、末尾のスラッシュも区別されます。
  - `SAMPLE_PORT` を `4175` から変えた場合も、組み込みの `sample-app` クライアントと一致しなくなります。
- **対処**: `redirect_uri` の文字列をそのまま Fixture の `clients[].redirectUris` にコピーします。

### 未知のクライアント

- **エラー**: `invalid_client`: `Unknown client.`
- **原因**: 次のいずれかです。
  - `client_id` が Fixture に登録されていない。
  - `POST /oauth/token` を `application/x-www-form-urlencoded` ではなく JSON ボディで送った。クライアントが HTTP Basic で認証している場合は、代わりに `unsupported_grant_type` が返ります。
- **対処**: クライアントを Fixture に登録します。トークンリクエストは `application/x-www-form-urlencoded` で送ります。

### クライアントシークレットが不正

- **エラー**: `invalid_client`: `Invalid client secret.`
- **原因**: `client_secret` がないか、間違っています。Calendarpit は PKCE フローも含め、すべてのトークンリクエストでクライアントシークレットを必須にしています。
- **対処**: 正しい `client_secret` を、フォームボディか HTTP Basic 認証で送ります。

### 認可コードが不正

- **エラー**: `invalid_grant`: `Invalid, expired, or mismatched authorization code.`
- **原因**: 次のいずれかです。
  - 認可コードが発行から 60 秒を過ぎている。
  - 認可コードがすでに 1 回使われている。
  - `redirect_uri` がないか、`/oauth/authorize` のときと違う。
  - クライアントが違う。
  - 接続が取り消された。
  - サーバーが再起動した。
- **対処**: 60 秒以内に 1 回だけ交換します。`/oauth/authorize` で使ったものと同じ `redirect_uri` とクライアントを使います。

### PKCE の verifier が不正

- **エラー**: `invalid_grant`: `Invalid PKCE verifier.`
- **原因**: `code_verifier` が `S256` の `code_challenge` と一致しないか、`[A-Za-z0-9._~-]` の 43〜128 文字になっていません。
- **対処**: `S256` のチャレンジを計算したときの元の verifier 文字列を送ります。

### リフレッシュトークンが不正

- **エラー**: `invalid_grant`: `Invalid refresh token.`
- **原因**: 次のいずれかです。
  - 接続が取り消された。
  - ユーザーが削除された。
  - ワークスペースがリセットされたか、Fixture が読み込まれた。
  - インメモリのサーバーが再起動した。
  - 別のクライアントのトークンを使った。
- **対処**: 認可フローをやり直して、新しい接続を作ります。

### コールバックで `state` がない、または一致しない

- **原因**: 次のいずれかです。
  - `/oauth/authorize` で `state` を省略した。`state` は送ったときだけそのまま返ります。
  - アプリを `localhost` で開始し、コールバックを `127.0.0.1` で受け取った。`state` を保存した Cookie は、ホスト名をまたいで共有されません。
- **対処**: `/oauth/authorize` に `state` を渡します。アプリと `redirect_uri` では同じホスト名（`127.0.0.1` か `localhost`）を使います。

### 未対応または未指定のスコープ

- **エラー**: `invalid_scope`: `Unsupported or missing scope.`
- **原因**: `scope` が空か、未対応のスコープ（`calendar.events.owned` や Drive のスコープなど）を含んでいます。
- **対処**: [対応しているスコープ](https://calendarpit.dev/ja/oauth.md)だけを要求します。

### 更新時にスコープを変更した

- **エラー**: `invalid_scope`: `Refresh scope changes are unsupported.`
- **原因**: `refresh_token` リクエストに `scope` パラメータが含まれており、最初に付与されたときの文字列と完全には一致していません。スコープの順序も区別されます。
- **対処**: トークンを更新するときは `scope` を省略します。

### トークンレスポンスに `refresh_token` がない

- **原因**: 次のいずれかです。
  - `/oauth/authorize` で `access_type=offline` を送っていない。
  - `refresh_token` グラントのレスポンスである。更新のレスポンスは新しい `access_token` だけを返し、元のリフレッシュトークンは有効なままです。
- **対処**: `/oauth/authorize` に `access_type=offline` を追加し、最初に受け取った `refresh_token` を保存しておきます。

### 未対応の認可パラメータ

- **エラー**: `/oauth/authorize` で `unsupported_feature`
- **原因**: リクエストに次のいずれかが含まれています。
  - `prompt=none`
  - `include_granted_scopes=true`
  - `nonce`、`login_hint`、`hd`、`enable_granular_consent` などの未対応のパラメータ
- **対処**: 未対応のパラメータを外します。対話的な同意（`prompt=consent` か `prompt=select_account`）を使います。

### PKCE のチャレンジが不正

- **エラー**: `invalid_request`: `PKCE requires S256.` または `Missing PKCE challenge.`
- **原因**: 次のいずれかです。
  - `code_challenge_method=S256` なしで `code_challenge` を送った（`plain` の場合も含む）。
  - `code_challenge` なしで `code_challenge_method` を送った。
  - `code_challenge` が 43 文字の base64url になっていない。
- **対処**: `code_challenge_method=S256` と、43 文字の base64url のチャレンジを使います。

### 同意リクエストの期限切れ、または使用済み

- **エラー**: `invalid_request`: `Consent request expired or already used.`
- **原因**: 次のいずれかです。
  - 同意画面を 5 分を超えて開いたままにした。
  - 同意画面を 2 回送信した。
  - 送信前にサーバーが再起動した。
- **対処**: `/oauth/authorize` から新しい認可リクエストを始めます。

### 保留中の同意リクエストが多すぎる

- **エラー**: `429 rateLimitExceeded`: `Too many pending consent requests.`
- **原因**: 保留中の同意リクエストがすでに 1,000 件あります。各リクエストは 5 分で期限切れになります。通常はテスト内のループが原因です。
- **対処**: 同意リクエストを完了させるか、新しく開くのをやめます。またはワークスペースをリセットします。

## Calendar API と SDK

### アクセストークンが不正または期限切れ

- **エラー**: `401 authError`: `Invalid or expired access token.`
- **原因**: 次のいずれかです。
  - `Authorization: Bearer` ヘッダーがないか、形式が不正。
  - アクセストークンの期限切れ（有効期間 1 時間）。
  - 接続の取り消し。
  - ワークスペースのリセット、または Fixture の読み込み。
  - インメモリのサーバーの再起動。
  - 本物の Google のトークンの送信。
- **対処**: `/oauth/token` でトークンを更新します。リセット、Fixture の読み込み、取り消しの後は、認可をやり直します。

### スコープが足りない

- **エラー**: `403 insufficientPermissions`: `Insufficient scope.`
- **原因**: トークンに必要なスコープがありません。たとえば次の場合です。
  - `.readonly` のスコープでイベントを書き込んだ。
  - `calendar.events` だけで `calendarList.list` を呼んだ。
  - `openid`、`email`、`profile` のいずれもなしで `/userinfo` を呼んだ。
- **対処**: エンドポイントに必要なスコープを要求して、認可をやり直します。詳しくは[ローカル OAuth](https://calendarpit.dev/ja/oauth.md)を参照してください。

### 未対応の API エンドポイントまたはメソッド

- **エラー**: `501 unsupportedFeature`: `Unsupported API endpoint or method.`。`/calendar/v3/calendar/v3/...` のようにパスが重複している場合も含みます。
- **原因**: 次のいずれかです。
  - 未実装の Google のエンドポイント（`acl`、`settings`、`colors`、`calendars.insert`、`events.move`）を呼んだ。
  - API のパスに `HEAD` を送った。一部のパスは、下記のとおり `HEAD` に別のエラーを返します。
  - `/users/me/calendarList/{calendarId}` に `POST` を送った。
  - ベース URL の設定ミスで、SDK が `/calendar/v3` を重複させた。
- **対処**: [Calendar API の互換性](https://calendarpit.dev/ja/compatibility.md)を確認します。SDK のベース URL は下の表のとおりに設定します。詳しくは[SDK の使用例](https://calendarpit.dev/ja/sdks.md)を参照してください。

次のパスでは、`HEAD` に別のエラーを返します。

- イベントのパスは `405 methodNotAllowed` を返します。
- `/calendars/{calendarId}` は `Unsupported calendar operation.` を返します。
- `/users/me/calendarList/{calendarId}` の既存エントリは `Unsupported calendar list operation.` を返します。

| SDK                             | ベース URL の設定                                                  |
| ------------------------------- | ------------------------------------------------------------------ |
| JS/TS（`@googleapis/calendar`） | `rootUrl` を `http://127.0.0.1:4173/` にします。                   |
| Kotlin                          | `rootUrl` をオリジンに、`servicePath` を `calendar/v3/` にします。 |
| PHP（`google/apiclient`）       | `$rootUrl` に `http://127.0.0.1:4173/` を渡します。                |
| Python、Go、C#                  | `/calendar/v3/` を含めます。                                       |

### 未対応のカレンダー操作またはカレンダーリスト操作

- **エラー**: `501 unsupportedFeature`: `Unsupported calendar operation.` または `Unsupported calendar list operation.`
- **原因**: 次のいずれかです。
  - `/calendar/v3/calendars/{calendarId}` に `PUT`、`PATCH`、`DELETE`、`HEAD` を送った。
  - `/calendar/v3/users/me/calendarList/{calendarId}` の既存エントリに、`HEAD` や `OPTIONS` などの未対応のメソッドを送った。そこへの `POST` は、代わりに `Unsupported API endpoint or method.` を返します。
- **対処**: カレンダーは管理画面か `/__calendarpit/calendars` で管理します。ユーザーごとの表示設定には `calendarList.patch` を使います。詳しくは[テスト制御 API](https://calendarpit.dev/ja/test-control.md)を参照してください。

### カレンダーが見つからない

- **エラー**: `404 notFound`: `Calendar not found.`
- **原因**: 次のいずれかです。
  - 存在しないカレンダー ID。
  - 別のテストユーザーが所有するカレンダー。
  - カレンダーを持たないユーザーでの `primary`。
  - `calendarList.delete` でユーザーのリストから外したカレンダー。これはカレンダーリストのメソッドの場合のみです。
- **対処**: 認証済みユーザーが所有するカレンダー ID を使うか、そのカレンダーの所有者として認可します。リストから外したエントリは `calendarList.insert` で戻せます。

### イベントが見つからない

- **エラー**: `404 notFound`: `Event not found.`
- **原因**: 次のいずれかです。
  - そのカレンダーに存在しないイベント ID。
  - `POST .../events/import` や `POST .../events/quickAdd`（`text` なし）を呼んだ。`import` や `quickAdd` がイベント ID として扱われます。
- **対処**: イベント ID を確認します。`import` や `quickAdd` の代わりに `events.insert` を使います。

### 未対応のイベントメソッド

- **エラー**: `405 methodNotAllowed`: `Unsupported event method.` または `Unsupported instances operation.`
- **原因**: イベントのルートに対して使えない HTTP メソッドを送りました。たとえば次の場合です。
  - `HEAD`
  - `/events` への `DELETE`
  - 既存の `/events/{eventId}` への `POST`
  - `/instances` への書き込み
- **対処**: 対応している HTTP メソッドとパスを使います。

### 未対応のクエリパラメータ

- **エラー**: `400 unsupportedFeature`: `Unsupported query parameter: <key>`
- **原因**: クエリパラメータがエンドポイントの許可リストにありません。たとえば `fields`、`q`、`eventTypes`、`sendNotifications`、`supportsAttachments`、`maxAttendees`、`text` です。
- **対処**: そのパラメータを外します。SDK が自動で付ける `fields` の部分レスポンスマスクも無効にします。

### 未対応のフィールド

- **エラー**: `400 unsupportedFeature`: `Unsupported field: <key>`
- **原因**: リクエストボディに未対応のフィールドがあります。たとえば次の場合です。
  - イベントの `eventType`、`attachments`、`source`、`htmlLink`。
  - FreeBusy の `items[]` で、`id` 以外のキーを持つエントリ。
- **対処**: 送信前に未対応のフィールドを取り除きます。詳しくは[Calendar API の互換性](https://calendarpit.dev/ja/compatibility.md)を参照してください。

### JSON 以外のレスポンス形式

- **エラー**: `400 invalidParameter`: `Only JSON responses are supported.`
- **原因**: クエリに `alt=media` か `alt=proto` が含まれています。
- **対処**: `alt` を省略するか、`alt=json` を使います。

### タイムスタンプにオフセットがない

- **エラー**: `400`: `Expected an RFC3339 timestamp with an offset.` または `dateTime requires an offset or timeZone.`
- **原因**: 次のいずれかです。
  - `timeMin`、`timeMax`、`updatedMin`、または FreeBusy の範囲に `Z` も `±HH:MM` もない。
  - イベントの `dateTime` にオフセットも `timeZone` もない。
- **対処**: `Z` か `±HH:MM` のオフセットを付けます。またはイベントの `start`/`end` に `timeZone` を指定します。

### ローカル時刻が不正またはあいまい

- **エラー**: `400`: `Invalid or ambiguous local time; supply an explicit offset.`
- **原因**: オフセットのない `dateTime` が、`timeZone` の夏時間の切り替えで存在しない時刻、または重複する時刻に当たっています。
- **対処**: `dateTime` に数値のオフセットを明示します。

### 繰り返しイベントのタイムゾーンや `UNTIL` のエラー

- **エラー**: `400`: `Recurring timed events require start.timeZone.` または `Timed recurrence UNTIL must be UTC.`
- **原因**: 時刻指定の繰り返しイベントに `start.timeZone` がないか、`UNTIL` の値が `Z` で終わっていません。
- **対処**: `start.timeZone` を設定します。時刻指定の `UNTIL` は UTC（`...Z`）で書きます。詳しくは[イベント、繰り返し、時刻の扱い](https://calendarpit.dev/ja/recurrence.md)を参照してください。

### 未対応の繰り返しルールや展開

- **エラー**: 繰り返しルールや展開で `400 unsupportedFeature`
- **原因**: 次のいずれかです。
  - ルールで `HOURLY` か `MINUTELY` を使った。
  - ルールに未対応の要素（`BYHOUR`、`BYWEEKNO`、`BYYEARDAY`）を含めた。
  - ルールにほかの iCalendar の行（`DTSTART`）を含めた。
  - 夏時間の切り替えで存在しない時刻に当たる回がある。
  - 終わりのないルールを `timeMax` なしで展開した。
  - 展開が 10,000 インスタンスか 100 年を超えた。
- **対処**: 対応している `DAILY`、`WEEKLY`、`MONTHLY`、`YEARLY` のルールを使います。`timeMax` を指定し、範囲も狭くします。詳しくは[イベント、繰り返し、時刻の扱い](https://calendarpit.dev/ja/recurrence.md)を参照してください。

### `singleEvents` なしの `orderBy=startTime`

- **エラー**: `400`: `startTime ordering requires singleEvents=true.`
- **原因**: `singleEvents=true` なしで `orderBy=startTime` を送りました。
- **対処**: `events.list` のクエリに `singleEvents=true` を追加します。

### 繰り返しの系列ではないイベント

- **エラー**: `400`: `Event is not a recurring series.`
- **原因**: 繰り返しではないイベントに `events.instances` を呼びました。
- **対処**: `events.instances` は繰り返しの系列の親イベントにだけ使います。

### 読み取り専用のフィールドを変更した

- **エラー**: `400`: `<key> is read-only.`
- **原因**: 次のいずれかです。
  - Calendar API の書き込みで `organizer`、`creator`、`iCalUID`、`recurringEventId`、`originalStartTime` を変更した。
  - `calendarList` の書き込みで `summary`、`timeZone`、`accessRole`、`primary`、`kind`、`deleted`、`conferenceProperties` を変更した。
- **対処**: そのフィールドを省略するか、既存の値を変えずに送ります。
  - ユーザーごとのカレンダー名は `summaryOverride` で設定します。
  - 独自の organizer 情報を設定するには、[Fixture](https://calendarpit.dev/ja/fixtures.md)か[テスト制御 API](https://calendarpit.dev/ja/test-control.md)を使います。

### `conferenceDataVersion` なしの `conferenceData`

- **エラー**: `400`: `conferenceData requires conferenceDataVersion=1.`
- **原因**: イベントのペイロードに `conferenceData` がありますが、`?conferenceDataVersion=1` がありません。
- **対処**: クエリ文字列に `conferenceDataVersion=1` を追加します。

### プライマリカレンダーはリストから外せない

- **エラー**: `403 cannotDeletePrimaryCalendar`: `Cannot remove the primary calendar from the calendar list.`
- **原因**: ユーザーのプライマリカレンダーに `calendarList.delete` を呼びました。
- **対処**: プライマリカレンダーはリストに残します。表示しない場合は `calendarList.patch`（`hidden: true` か `selected: false`）を使います。

### 招待されたユーザーの編集が拒否される

- **エラー**: `403 forbiddenForNonOrganizer`
- **原因**: 招待されたユーザーが招待のコピーを編集する際に、次のいずれかを行いました。
  - `PUT` を使った。
  - 共有されているイベントのフィールドを変更した。
  - 別の参加者を編集した。
- **対処**: `PATCH` を使います。自分の `responseStatus`/`comment`、`reminders`、`colorId`、`transparency`、`extendedProperties` だけを更新します。

### イベントやカレンダーリストのエントリの重複

- **エラー**: `409 duplicate` と、次のいずれかのメッセージ
  - `Duplicate event ID.`
  - `Invitation event ID conflicts with an existing local event.`
  - `Calendar already exists in the calendar list.`
- **原因**: 次のいずれかです。
  - `events.insert` がそのカレンダーの既存の ID を使った。
  - 参加者への招待のコピーが、参加者のプライマリカレンダーにある同じ ID で `iCalUID` が異なるイベントと衝突した。たとえば、Fixture やテスト制御 API で `iCalUID` を設定したイベントです。同じ ID でデフォルトの `iCalUID` を持つイベントは、招待のコピーで上書きされます。
  - すでにリストにあるカレンダー（`primary` を含む）に `calendarList.insert` を呼んだ。
- **対処**: 一意のイベント ID を使います。`calendarList.insert` は、リストにないカレンダーにだけ呼びます。

### 削除済みのイベント

- **エラー**: `410 deleted`: `Event is already deleted.`
- **原因**: すでに `cancelled` になっているイベントに `events.delete` を呼びました。
- **対処**: 削除時の `410` は、削除済みとして扱います。

### `etag` の不一致

- **エラー**: `412 conditionNotMet`: `Event has changed.` または `Calendar list entry has changed.`
- **原因**: `If-Match` ヘッダーが、イベントまたはカレンダーリストのエントリの現在の `etag` と一致しません。
- **対処**: `events.get` か `calendarList.get` で取得し直してから、再試行します。

### リクエストボディが大きすぎる

- **エラー**: `413 payloadTooLarge`: `Request body too large.`
- **原因**: リクエストボディが 1,048,576 バイト（1 MiB）を超えています。
- **対処**: ペイロードを小さくします。

### `maxResults` が範囲外

- **エラー**: `400`: `maxResults must be between 1 and 2500.`
- **原因**: `maxResults` が `0`、負の値、または `2500` より大きい値です。
- **対処**: `maxResults` を `1` から `2500` の範囲にします。

### Python SDK が Google から discovery ドキュメントを取得する

- **原因**: `googleapiclient.discovery.build` が Google から discovery のメタデータをダウンロードしようとしました。
- **対処**: `google-api-python-client` 2.x を使い、`static_discovery=True` と `cache_discovery=False` を指定します。リモートの discovery URL は設定しません。詳しくは[SDK の使用例](https://calendarpit.dev/ja/sdks.md)を参照してください。

## 差分同期とページネーション

### 同期トークンが不正または期限切れ

- **エラー**: `410 fullSyncRequired`: `Sync token is invalid or expired. Perform a full synchronization.`
- **原因**: 次のいずれかです。
  - `syncToken` が 7 日より古い。
  - ワークスペース内のトークンが 100 個を超え、`syncToken` が破棄された。
  - `syncToken` が別のカレンダーやユーザーのものである。
  - リセット、Fixture の読み込み、インメモリでの再起動で `syncToken` が消えた。
- **対処**: ローカルの同期状態を破棄し、`syncToken` なしの `events.list` でフル同期をやり直します。詳しくは[差分同期](https://calendarpit.dev/ja/sync.md)を参照してください。

### 同期のクエリオプションが変わった

- **エラー**: `400`: `Sync query options must match the initial synchronization.`
- **原因**: `singleEvents`、または実際に使われる `timeZone` が最初の同期と違います。`timeZone` を省略していて、カレンダーのタイムゾーンが変わった場合も含みます。
- **対処**: 最初のフル同期と同じ `singleEvents` と `timeZone` を指定します。

### `syncToken` と組み合わせられないパラメータ

- **エラー**: `400` と、次のいずれかのメッセージ
  - `<key> cannot be combined with syncToken.`
  - `Incremental synchronization must include deleted events.`
  - `instances does not support syncToken.`
- **原因**: 次のいずれかです。
  - `syncToken` を `timeMin`、`timeMax`、`orderBy`、`updatedMin`、`iCalUID`、拡張プロパティのフィルタと一緒に送った。
  - `syncToken` を `showDeleted=false` と一緒に送った。
  - `syncToken` を `events.instances` で使った。
- **対処**: `events.list` には `syncToken` だけを送ります。一緒に送れるのは、最初と同じ `singleEvents` と `timeZone`、`maxResults`、`pageToken`、`showDeleted=true` です。

### ページトークンが不正または期限切れ

- **エラー**: `410 invalidPageToken`: `Invalid or expired page token.` または `Invalid or stale page token.`
- **原因**: ページトークンが無効になっています。
  - イベントの `pageToken` が期限切れになった（有効期間 1 時間）、破棄された（固定されたページが 1,000 を超えた）、またはクエリのオプションの変更やサーバーの再起動・リセットで無効になった。
  - ワークスペースのデータに書き込みがあった後に、`calendarList` の `pageToken` を使った。
- **対処**: 最初のページから一覧を取得し直します。

## watch 通知

### watch のホストが許可されていない

- **エラー**: `400`: `Watch host is not allowed. Configure CALENDARPIT_WATCH_HOSTS explicitly.`
- **原因**: コールバックの `address` のホスト名が、`127.0.0.1`、`[::1]`、`localhost` のいずれでもありません。たとえば `host.docker.internal` やコンテナのサービス名です。
- **対処**: `CALENDARPIT_WATCH_HOSTS=<host1>,<host2>` を設定して Calendarpit を起動します。ホスト名だけを指定し、ポートは含めません。詳しくは[watch 通知](https://calendarpit.dev/ja/watch.md)を参照してください。

### watch のコールバック URL が不正

- **エラー**: `400`: `Invalid watch callback URL.`
- **原因**: `address` が `http:` でも `https:` でもないか、認証情報や `#fragment` を含んでいます。
- **対処**: 認証情報やフラグメントを含まない `http://` か `https://` のコールバック URL を使います。

### チャネルのフィールドが不正

- **エラー**: `400` と、次のいずれかのメッセージ
  - `Only web_hook channels are supported.`
  - `Invalid channel ID.`
  - `Invalid channel token.`
  - `Invalid expiration.`
- **原因**: 次のいずれかです。
  - `type` が `"web_hook"` ではない。
  - `id` が `[A-Za-z0-9_-]{1,64}` に一致しない。
  - `token` が 256 文字を超えるか、改行を含む。
  - `expiration` が未来の Unix ミリ秒の整数ではない。
- **対処**: `events.watch` か `calendarList.watch` のリクエストボディで、不正なフィールドを直します。

### チャネル ID の重複

- **エラー**: `409 duplicate`: `Duplicate channel ID.`
- **原因**: ワークスペース内の別の有効な watch チャネルが、すでにその `id` を使っています。
- **対処**: チャネルごとに新しい一意の `id`（UUID など）を生成します。

### watch は成功するが、アプリに通知が届かない

- **原因**: Webhook の受信側への配信に失敗しています。3 回試しても失敗すると、その配信は `failed` になります。受信側が次のいずれかに当たると、その試行は失敗します。
  - 接続できない。
  - 2,000 ms 以内に応答しない。
  - `200`、`201`、`202`、`204` 以外のステータスを返す。3xx のリダイレクトはたどりません。
- **対処**: 管理画面の **Watch**（`http://127.0.0.1:4173/watch`）で、配信ログの状態と HTTP ステータスコードを確認します。テストでは `POST /__calendarpit/deliveries/drain` を呼び、待機中の配信が終わるまで待ちます。

### 再起動、リセット、取り消しの後に通知が届かなくなる

- **原因**: 次のいずれかです。
  - 再起動すると、キューにある配信は取り消されます。
  - インメモリでの再起動では、チャネルも消えます。
  - コールバックのホストが許可されなくなった状態で再起動すると、チャネルが消えます。
  - リセット、Fixture の読み込み、接続の取り消しでは、チャネルが停止します。
  - カレンダーを削除すると、そのカレンダーのイベントのチャネルが停止します。
  - チャネルの期限が切れました。期限はデフォルトで 24 時間、最大 7 日です。
- **対処**: 再起動の後は状態を取得し直します。チャネルが停止したか期限切れになっている場合は、新しい watch チャネルを登録します。

### `channels.stop` でチャネルが見つからない

- **エラー**: `channels.stop` で `404 notFound`: `Channel not found.`
- **原因**: 次のいずれかです。
  - `resourceId` が違う。
  - チャネル ID が存在しない。
  - チャネルがすでに停止したか、期限切れ後に削除されている。
  - チャネルを作成した接続とは別の接続のトークンで、`channels.stop` を呼んだ。
- **対処**: チャネルを作成した接続のトークンと、一致する `resourceId` で `channels.stop` を呼びます。期限切れでも、まだ削除されていないチャネルには `204` が返ります。[watch 通知](https://calendarpit.dev/ja/watch.md)を参照してください。

## 管理 API

### 管理 API の書き込みには JSON が必要

- **エラー**: `415 unsupportedMediaType`: `Management writes require application/json.`
- **原因**: `/__calendarpit/*` への `POST`、`PATCH`、`PUT`、`DELETE` リクエストを `Content-Type: application/json` なしで送りました。`/reset` や `/deliveries/drain` のようにボディのない呼び出しも対象です。
- **対処**: 管理 API へのすべての書き込みリクエストに `-H 'Content-Type: application/json'` を付けます。詳しくは[テスト制御 API](https://calendarpit.dev/ja/test-control.md)を参照してください。

### 別オリジンからの管理リクエスト

- **エラー**: `403 forbidden`: `Cross-origin management requests are not allowed.`
- **原因**: 別オリジンのブラウザのスクリプトが `/__calendarpit/*` を呼びました。
- **対処**: `/__calendarpit/*` は、テストランナー、CLI、バックエンドから、別オリジンの `Origin` ヘッダーを付けずに呼びます。または管理画面を使います。

### state のレスポンスを Fixture として送った

- **エラー**: `POST /__calendarpit/fixtures` で `400 unsupportedFeature`: `Unsupported field: storage`
- **原因**: `GET /__calendarpit/state` のレスポンスを、そのまま Fixture として送りました。
- **対処**: `users`、`calendars`、`events`、`clients` だけを持つ正しい Fixture のオブジェクトを送ります。`clients` にはクライアントの `secret` の値を含めます。詳しくは[Fixture](https://calendarpit.dev/ja/fixtures.md)を参照してください。
