ヘルプ

トラブルシューティング

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 と同じ形式でエラーを返します。

{
  "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 を返します。
{
  "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: を使います。ストレージと再起動を参照してください。

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

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

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

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

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 のルールを参照してください。

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

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

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

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

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

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

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 に設定します。詳しくはアプリを接続するを参照してください。

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

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

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 のスコープなど)を含んでいます。
  • 対処: 対応しているスコープだけを要求します。

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

  • エラー: 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を参照してください。

未対応の 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 の互換性を確認します。SDK のベース URL は下の表のとおりに設定します。詳しくはSDK の使用例を参照してください。

次のパスでは、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/ にします。
KotlinrootUrl をオリジンに、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を参照してください。

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

  • エラー: 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 の互換性を参照してください。

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)で書きます。詳しくはイベント、繰り返し、時刻の扱いを参照してください。

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

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

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かテスト制御 APIを使います。

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 の使用例を参照してください。

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

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

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

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

  • エラー: 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 通知を参照してください。

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 通知を参照してください。

管理 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を参照してください。

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

  • エラー: 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を参照してください。

目次

エラーレスポンスの形式Google API のエラー形式OAuth のエラー形式Calendarpit の起動ポートが使用中データベースが使用中Fixture ファイルが見つからないFixture のパスがディレクトリになっているFixture ファイルを読み取れない・JSON として不正Fixture の検証に失敗する再起動しても Fixture の変更が反映されない起動方法を変えたらデータが見えないDocker でブラウザ、バックエンド、Webhook の受信先に接続できないCALENDARPIT_* の値が反映されないオプションの値が不正、未知のオプションやコマンドOAuth本物の Google の同意画面が表示されるトークンの交換や更新で Google に接続してしまうGoogle のデフォルトの OAuth パスで 501 になる登録されていないリダイレクト URI未知のクライアントクライアントシークレットが不正認可コードが不正PKCE の verifier が不正リフレッシュトークンが不正コールバックで state がない、または一致しない未対応または未指定のスコープ更新時にスコープを変更したトークンレスポンスに refresh_token がない未対応の認可パラメータPKCE のチャレンジが不正同意リクエストの期限切れ、または使用済み保留中の同意リクエストが多すぎるCalendar API と SDKアクセストークンが不正または期限切れスコープが足りない未対応の API エンドポイントまたはメソッド未対応のカレンダー操作またはカレンダーリスト操作カレンダーが見つからないイベントが見つからない未対応のイベントメソッド未対応のクエリパラメータ未対応のフィールドJSON 以外のレスポンス形式タイムスタンプにオフセットがないローカル時刻が不正またはあいまい繰り返しイベントのタイムゾーンや UNTIL のエラー未対応の繰り返しルールや展開singleEvents なしの orderBy=startTime繰り返しの系列ではないイベント読み取り専用のフィールドを変更したconferenceDataVersion なしの conferenceDataプライマリカレンダーはリストから外せない招待されたユーザーの編集が拒否されるイベントやカレンダーリストのエントリの重複削除済みのイベントetag の不一致リクエストボディが大きすぎるmaxResults が範囲外Python SDK が Google から discovery ドキュメントを取得する差分同期とページネーション同期トークンが不正または期限切れ同期のクエリオプションが変わったsyncToken と組み合わせられないパラメータページトークンが不正または期限切れwatch 通知watch のホストが許可されていないwatch のコールバック URL が不正チャネルのフィールドが不正チャネル ID の重複watch は成功するが、アプリに通知が届かない再起動、リセット、取り消しの後に通知が届かなくなるchannels.stop でチャネルが見つからない管理 API管理 API の書き込みには JSON が必要別オリジンからの管理リクエストstate のレスポンスを Fixture として送った