SharePoint Online getChanges APIで期限切れchangeTokenの挙動と取りこぼし対策

SharePoint Online のリスト同期で getChanges(REST) を使うと、古い changeToken を渡しても例外が出ず、結果が欠落するケースがあります。この記事では、ドキュメントとの違いを整理し、変更取りこぼしを防ぐための実装・運用パターンを具体的に解説します。

目次

結論:SharePoint Online の getChanges(REST) は「期限切れでも成功してしまう」前提で設計する

SharePoint の変更ログ(Change Log)は永遠に保持されるものではなく、一定期間を過ぎるとローテーションされます。古いドキュメントでは「変更ログは既定で 60 日程度で削除され、開始トークンが無効なら SPInvalidChangeTokenException が発生する」と説明されています。

ところが SharePoint Online の REST エンドポイント(Atom フィードで返ってくる _api/.../getchanges)では、60 日以上前の changeToken を渡しても例外にならず、成功レスポンスのまま「保持期間内に残っている分だけ(または空)」が返る、という報告が出ています。

この挙動は、2025 年 9 月時点で Microsoft Learn の Q&A でも再現報告があり、sp-dev-docs の GitHub Issue では “bug-confirmed” のラベルが付与されています。つまり「例外が出ない=安全」ではなく、アプリ側で期限切れ・ギャップを検知し、リカバリ(取り直し)できる仕組みを入れるのが現実的です。

まず押さえる:getChanges と changeToken は何を保証しているか

getChanges は、SharePoint が内部で保持している「変更ログ」を条件付きで読み出す API です。対象はリスト(リストアイテム)だけでなく、サイト・Web・フォルダーなどスコープを変えて取得できます。

取得に使う changeToken は「どこから変更ログを読み始めるか」を示す“しおり”です。通常は、前回の応答で返ってきた最後の changeToken を保存しておき、次回の呼び出しで ChangeTokenStart に渡して差分だけを取得します。

ドキュメント上の基本ルール

古い(オンプレミス中心の)ドキュメントでは、次のようなルールが明記されています。

  • 変更ログは既定で 60 日程度で期限切れになり、タイマージョブが削除する
  • 開始トークンが「現在の変更ログの開始より前」を指す場合、例外が発生し得る
  • GetChanges / List.GetChanges は無効な changeToken で SPInvalidChangeTokenException(または関連 SPException)を投げる

REST でのレスポンス形式は JSON と Atom を選べる

SharePoint REST は Accept ヘッダーで JSON と Atom(XML) を切り替えられます。getChanges の挙動確認をするときは、どの形式で見ているか(Atom なのか JSON なのか)も統一しておくと、再現や比較がしやすくなります。

観点ドキュメントで想定される挙動SharePoint Online REST で観測される挙動システムへの影響
古い changeToken を渡したとき無効なら例外(SPInvalidChangeTokenException 等)成功(200)で Atom/JSON が返ることがある例外リトライ頼みだと取りこぼしに気付けない
変更ログの保持既定 60 日程度(調整可能)保持期間は明示されず、古いログは取得できない長期間停止すると差分同期は成立しない
「開始より前」のトークン例外が発生し得るREST では例外にならず“残っている範囲だけ”になる可能性ギャップ検知・フル再同期が必要

実際に報告されている挙動:期限切れでも例外が出ず、古い変更も返らない

Microsoft Q&A では、60 日以上前に取得した changeToken を渡したところ、例外は出ず Atom フィード(成功レスポンス)が返却された、という報告があります。

同内容は sp-dev-docs(SharePoint 開発者向けドキュメントの GitHub)にも Issue として起票され、SharePoint Online + REST API の文脈で、期待する例外が返らない点が問題提起されています。さらに当該 Issue には “Confirmed bug, not working as designed / expected.” のラベルが付いています。

ここで重要なポイント

  • 「例外が出ない」こと自体が、changeToken が有効であることを意味しない
  • 「60 日より前の変更が返らない」ことは、変更ログがローテーションされている(またはそれ相当の制約がある)可能性を示唆する
  • アプリ側が “例外が出たらリセット” という設計だと、静かに欠落が起きうる

なぜこうなるのか:REST 実装の差と「ベストエフォート」処理

SharePoint Online はマルチテナントサービスであり、オンプレミスの SharePoint Server とは内部実装も運用制約も異なります。その結果、同じ概念(Change Log / ChangeToken)を扱っていても、REST のエラーハンドリングが CSOM/オンプレミスの記述と一致しないケースが出てきます。

Q&A の回答でも「CSOM/オンプレ向けの古い記述に比べ、SharePoint Online REST では挙動が異なる可能性がある」「公開情報だけでは仕様かバグか断定できないためサポートで確認を」と説明されています。

また、「ここ 1〜2 年で挙動が変わったのか?」という問いに対しては、少なくとも公開情報の範囲では明確な仕様変更の根拠は見当たらない、とも述べられています。

結局のところ、アプリ設計上は次のように割り切るのが安全です。

  • changeToken は“いつでも有効”ではない(保持期間・内部都合で無効化される)
  • 無効化されたときに API が必ず例外で教えてくれる保証はない
  • 取得できない期間が生まれうる前提で、復旧戦略(フル再同期・監査ログなど)を用意する

取りこぼしが発生する典型パターンを整理する

「いつ」「どんな条件」で欠落が起きるかを先に整理すると、対策がシンプルになります。

パターン起きること検知のコツ推奨アクション
ポーリング停止が保持期間を超える古い変更ログが消え、差分取得が不可能保存している changeToken の“時刻”が古いフル再同期 + 欠落区間の扱いを明確化
期限切れでも API が成功する例外が出ず、保持範囲だけ返る/空になる「前回時刻より前の変更が返らない」「レスポンスが急に少ない」アプリ側で期限判定してリセット
リスト/ライブラリの大量変更・高負荷応答が分割され、途中で止めると欠落LastChangeToken をバッチごとに確実に保存ページング・再開可能な実装に
プロパティ名の大文字小文字ミス期待した変更種別が返らないChangeQuery のプロパティはケースセンシティブDeleteObject/Update など正確な名前で送る

対策の全体像:期限切れ判定・リカバリ・監視をセットで入れる

getChanges を差分同期に使うなら、最低限次の 3 点は“セット”で実装するのがおすすめです。

対策狙い実装ポイント
changeToken の“時刻”を取り出して期限判定期限切れを例外に頼らず検知トークン文字列を解析し、しきい値より古ければ危険扱い
リセット手順(フル再同期/ベースライン再構築)欠落リスクを限定し、運用で復旧可能に「リセットしたら何を正とするか」を仕様化(重複処理も許容)
監視・アラート静かな欠落を早期に発見ポーリング間隔、取得件数の急減、トークン時刻の経過を監視

changeToken の時刻を解析して期限切れを判断する

Microsoft Learn のガイダンスでは、ChangeToken.StringValue はセミコロン区切りで、バージョン、スコープ、GUID、日時(Ticks)、アイテム値を並べる、と説明されています。サンプルでは DateTime.Now.AddDays(-2).ToUniversalTime().Ticks を埋め込んでいます。

つまり「大きな数値部分」は、.NET の DateTime.Ticks(0001-01-01 からの 100ns 単位)であるケースが一般的です。運用上は、ここを読み取って「今から見てどれだけ古いか」を計算できます。

実装の考え方(安全側)

  • 保持期間は固定ではない可能性があるため、しきい値は余裕を持って設定する(例:60 日を根拠にするなら 45 日で警戒、50 日で強制リセット など)
  • “たまたま変更が少ない”と“欠落している”は区別しづらいので、時刻ベースの判定を最優先にする
  • トークン解析に失敗したら「危険扱い」でリセットする(フェイルセーフ)

C# 例:changeToken から UTC 時刻を取り出す

using System;
using System.Globalization;

public static class SharePointChangeTokenUtil
{
// SharePoint の changeToken 文字列から「日時」を推定して取り出す
// 例: "1;3;;638664757420130000;1288941838"
public static bool TryGetTokenTimeUtc(string token, out DateTime tokenTimeUtc)
{
tokenTimeUtc = default;


    if (string.IsNullOrWhiteSpace(token))
    {
        return false;
    }

    var parts = token.Split(';');
    if (parts.Length < 4)
    {
        return false;
    }

    // 一般的には 4 つ目が DateTime.Ticks
    if (!long.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var ticks))
    {
        return false;
    }

    // DateTime の有効範囲チェック(0001-01-01 〜 9999-12-31)
    if (ticks < DateTime.MinValue.Ticks || ticks > DateTime.MaxValue.Ticks)
    {
        return false;
    }

    tokenTimeUtc = new DateTime(ticks, DateTimeKind.Utc);
    return true;
}


}

期限切れ判定の例(擬似コード)

var now = DateTime.UtcNow;
var window = TimeSpan.FromDays(45); // 例: 保持 60 日を想定するなら安全側に 45 日で警戒
if (!TryGetTokenTimeUtc(savedToken, out var tokenTimeUtc) || tokenTimeUtc < now - window)
{
    // 期限切れ(または解析不能)なので、リセットモードへ
}

期限切れと判断したらどうするか:3つのリカバリ戦略

ここが“取りこぼし対策”の核心です。期限切れが疑われた時点で、差分同期の前提が崩れているので、システムの目的に合わせて復旧パターンを選びます。

戦略A:getChanges を「現在のログの先頭」から読み直す(最小コスト)

ChangeTokenStart を null にして呼ぶと、現在保持されている変更ログの先頭から取得する、というルールがドキュメントにあります。

ただし、既にローテーションで消えた期間は取り戻せません。欠落区間を“なかったことにできる”用途(例:定期的なキャッシュ更新、多少の欠落が許容される集計)向きです。

戦略B:リスト全件を読み直してベースラインを再構築(安全側)

欠落を許容できないなら、リストアイテムを全件取り直し、外部ストアの状態を SharePoint に合わせるのが最も確実です。コストは高いですが、結果の整合性が取り戻せます。

  • 外部ストア側は “Upsert(存在すれば更新、なければ追加)” で受ける
  • 削除の扱いが必要なら「差分で消えたもの」を判断できる設計(スナップショット比較等)を用意する

戦略C:Webhook/別ログと組み合わせて欠落区間を埋める(上級)

SharePoint Online には Webhook があり、通知をトリガーにして変更を取りにいく設計が可能です。通知自体が取りこぼしたときのために、変更ログ(getChanges)で追いかける“補完”として組み合わせるのが定番です。

さらに長期的な追跡が必要なら、Microsoft Purview の監査ログ(保持ポリシー)という選択肢もあります。変更ログより長く保持できる可能性がありますが、用途・ライセンス・取得手段の設計が別軸になるため、要件が強い場合のオプションと捉えるのが現実的です。

REST getChanges の呼び出し実装でハマりやすいポイント

ChangeQuery のプロパティ名はケースセンシティブ

REST の getChanges は、ChangeQuery をボディで POST しますが、プロパティ名の大文字小文字が違うとエラーになったり、期待した変更が返らなかったりします。例えば DeleteObject は deleteObject では認識されません。

POST ボディ例(JSON で取得する場合)

POST https://{tenant}.sharepoint.com/sites/{site}/_api/web/lists(guid'{listGuid}')/getchanges
Accept: application/json;odata=verbose
Content-Type: application/json;odata=verbose

{
"query": {
"__metadata": { "type": "SP.ChangeQuery" },
"Item": true,
"Add": true,
"Update": true,
"DeleteObject": true,
"ChangeTokenStart": {
"__metadata": { "type": "SP.ChangeToken" },
"StringValue": "1;3;{listGuid};638664757420130000;-1"
}
}
}

この例のように「自分で作る開始トークン」は末尾(アイテム値)を -1 にするパターンが一般的です。一方で、サーバー応答で返ってくる changeToken は末尾が数値になっていることが多いため、差分同期ではサーバーから返ったトークン文字列を丸ごと保存し、次回も丸ごと渡すのが基本です。

なお Atom で見たい場合は Accept: application/atom+xml のように指定します。

「削除」を追いたい場合の注意

削除は DeleteObject を true にして取得するのが基本ですが、削除後にアイテム本体が存在しないため、外部ストアの削除処理は「ID だけで削除できる」ようにしておくと運用が安定します。また、フル再同期戦略を採る場合は、削除検知の仕組み(スナップショット差分、別ログの参照等)を最初から設計に組み込むのが重要です。

安全に差分同期するための実装パターン(チェックポイント方式)

getChanges を使う同期処理は、データ処理としては “ストリーム” に近い性質があります。途中で落ちても続きから再開できるように、チェックポイント(保存する changeToken)の扱いを厳密にします。

推奨フロー

  1. 前回保存した changeToken を読み込む
  2. changeToken を解析して「古すぎる」ならリセットモードへ(フル再同期 or 現ログ先頭)
  3. getChanges を呼ぶ
  4. 返ってきた変更を 1 件ずつ処理する(外部ストアへ反映)
  5. 処理が完了した changeToken を永続化する(同一トランザクションでコミット)
  6. 空になるまで繰り返す

特に重要なのは「処理が完了した changeToken だけ保存する」ことです。先にトークンだけ進めると、途中失敗で欠落が確定します。

運用監視の指標

  • 最終成功時刻(ポーリングが止まっていないか)
  • 保存トークンの時刻(しきい値を超えそうならアラート)
  • 1 回あたり取得件数(急減・急増は異常の兆候)
  • API の 429/503 などスロットリング傾向(バックオフ制御)

代替案:Microsoft Graph の delta を検討する価値

「差分同期」という目的だけを見ると、Microsoft Graph の delta クエリ(変更追跡)のほうが、設計思想としては分かりやすいケースがあります。delta は初回に全件を列挙し、以降は @odata.deltaLink を使って変更だけを取得するパターンです。

SharePoint のリストアイテムにも delta が用意されており、全件同期→差分同期の流れを組みやすくなっています。

一方で、deltaLink は「そのまま保存して、その URL をそのまま次回呼ぶ」ことが前提になっており、途中で勝手にクエリを付け足すと動かない等の制約があります。最近も listItem の deltaLink/トークン周りの仕様差分に関する Q&A が出ているため、実装時はドキュメント追従が必須です。

どうしても公式見解が必要な場合

「自社システムの監査要件として、取りこぼしが絶対に許されない」「この挙動が仕様かバグかを公式に確定したい」という場合は、Microsoft 365 管理センターからサポートチケットを起票して確認するのが最短です。Q&A の回答でもその必要性が示唆されています。

最後に:実務で迷わないためのチェックリスト

  • ポーリング間隔は保持期間をまたがない(停止を許容しない)
  • changeToken の時刻を解析し、古ければ危険扱いでリセット
  • チェックポイントは「処理完了後」にだけ保存する
  • リセット時の復旧戦略(フル再同期 or 現ログ先頭)を要件で決める
  • 削除・権限変更など「後から復元できない変更」ほど別経路(Webhook/監査ログ等)も検討

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次