Outlook の Office アドインで取得したイベントIDを保存し、後から Microsoft Graph API(GET /me/events/{id})で予定を取得しようとしたら、IDに「/」が含まれて失敗する――。本記事では原因の整理と、iCalUId検索・ID形式変換で安全に取得/更新する実践手順をまとめます。
現象:イベントIDに「/(スラッシュ)」が入ると Graph のイベント取得が失敗する
Outlook の Office アドイン(Office.js)で取得した予定(イベント)のIDをそのままデータベースに保存し、後から Web アプリ側で Microsoft Graph API の GET /me/events/{event-id} を叩いて詳細を取ろうとすると、イベントIDの中に「/」が含まれているケースで呼び出しが失敗することがあります。URL のパスは「/」を区切りとして解釈するため、IDをパスに直書きした瞬間にパス構造が壊れてしまうのが直感的な原因です。
厄介なのは、一般的な URL エンコード(%2F)を試しても期待どおりに動かないことがある点です。アプリ・プロキシ・API ゲートウェイ・サーバー側のルーティング実装によっては、URL デコードのタイミングやセキュリティ上の制限により「エンコードされたスラッシュ」を受け付けないことがあり、結果として Graph へのリクエストが成立しません。
| やりたいこと | よくある実装 | 起きがちな問題 | 安全な方向性 |
|---|---|---|---|
| 予定詳細を取得 | /me/events/{event-id} | IDに / が混ざるとパス解釈で失敗 | iCalUId で検索 → id を取り直す |
| 予定を更新/削除 | 取得した id を使って PATCH/DELETE | 元のIDが Graph 用でない(EWS 形式など)と不整合 | ID形式を変換(convertToRestId / translateExchangeIds) |
| 一時しのぎ | / を - に置換 | ID衝突や再現性の不安、将来の仕様変更耐性が弱い | 置換ではなく「検索」か「公式の変換」を採用 |
まず押さえるべきポイント:そのIDは「Graph の event.id」ではない可能性がある
「Outlook のアドインで取ったID」と「Graph が /me/events/{id} で期待しているID」は、同じに見えて別物であることがあります。特に Outlook アドインが返す itemId は、環境や API の使い方により EWS 形式のIDであることがあり、この形式は URL にそのまま埋め込むことを前提にしていません。
| IDの種類 | 代表的な入手元 | 見た目の特徴(例) | URL パス安全性 | 主な用途 |
|---|---|---|---|---|
| EWS ID | Office アドインの itemId(既定/設定次第) | 長い文字列、+ や / を含むことがある | 低い(そのままだと事故りやすい) | EWS/Exchange 系APIとの連携 |
| REST/Graph 向け ID(restId など) | Office.js の変換API、Graph の変換API | URL-safe 化された表現(環境により記号が変化) | 高い(URLに載せやすい) | Outlook REST API / Graph での操作 |
| Graph event.id | Graph のイベント取得結果 | opaque(意味を推測しない) | 原則は高いが、取り回しを誤ると失敗する場合も | Graph リソースの直接参照 |
| iCalUId | Graph のイベントプロパティ、アドインから参照できる場合も | iCalendar の UID(予定の識別子) | 高い(クエリ文字列内で扱う) | 検索・突合(「見つける」用途) |
この手のトラブルで最初にやるべきことは、「保存しているIDがどの形式か」を確かめることです。形式がズレているなら、URL エンコードで頑張るのではなく、公式の変換(または検索経由)に寄せた方が、運用の再現性と将来耐性が上がります。
解決策の本命:イベントIDをパスに直書きせず、iCalUId で検索して取得する
Microsoft Learn の Q&A で提案されている現実解は、イベントを id で直接引かずに iCalUId で検索して取る方法です。これなら、URL パスの「/」問題を根本から回避できます。
基本形:iCalUId でフィルターしてイベントを取得する
Web アプリ側では、次のように iCalUId を条件にイベントを検索します(id をパスに入れないのがポイントです)。
GET https://graph.microsoft.com/v1.0/me/events?$filter=iCalUId eq '{iCalUId}'
検索結果が1件に定まるなら、そのレスポンスに含まれる id を「Graph が期待する形式」として扱えます。以後の取得・更新・削除は、その id を使うのが安全です。
定期予定(繰り返し)の注意:同じ iCalUId が複数イベントに紐づくことがある
ここで気をつけたいのが、定期予定(繰り返し)や例外(1回だけ変更/キャンセル)です。これらは iCalUId が同一になることがあり、iCalUId だけだと候補が複数返る可能性があります。対策としては、開始日時(start)などの追加条件で絞り込むのが実務的です。
| ケース | iCalUId だけ検索すると | 追加で持っておくと強い情報 | おすすめの絞り込み方 |
|---|---|---|---|
| 単発の予定 | ほぼ1件 | 開始日時、主催者、件名 | iCalUId で 1件取得 → その id を採用 |
| 定期予定(シリーズ) | 複数(シリーズ本体/インスタンス/例外) | start、originalStart、seriesMasterId | 候補を開始日時で突合して 1件に定める |
| 例外(1回だけ変更) | 複数の中に混ざる | 変更前の日時(originalStart) | 元の発生日時で合わせて例外を特定する |
実装のコツ:検索結果を小さく、突合は確実に
運用を安定させるためのコツは次のとおりです。
$selectで必要項目だけ取得(通信量と処理量を減らす)- 開始日時の比較はタイムゾーンを統一(UTC で保存し、UTC で比較する)
- 複数ヒットしたら「正しい1件」を決めるルールを用意(最初の1件採用など曖昧な実装は事故の元)
GET https://graph.microsoft.com/v1.0/me/events?$filter=iCalUId eq '{iCalUId}'&$select=id,subject,start,end,iCalUId,seriesMasterId,type
この時点で得られた id は Graph が返した値なので、更新・削除に回す際も整合しやすくなります(ただし、後述のとおり「どのAPIで取ったIDか」を混ぜないことが重要です)。
更新・削除まで視野に入れるなら:iCalUId → id 取得 → id で操作、の2段階が堅い
iCalUId はあくまで「探す」ためのキーとして優秀です。一方で、Graph の操作(PATCH/DELETE)では多くの場合 /me/events/{id} のように id が必要になります。そこで現場で採用しやすいのが、次の2段階運用です。
| 段階 | 目的 | 呼び出し例 | DBに残すと良いもの |
|---|---|---|---|
| 検索 | 対象イベントを特定し Graph の id を取得 | GET /me/events?$filter=iCalUId eq '...' | id(Graph由来), iCalUId, start |
| 操作 | 更新/削除/詳細取得 | GET/PATCH/DELETE /me/events/{id} | id を第一キーとして利用 |
ポイントは、更新・削除の直前に、いったん iCalUId で “最新の id” を取り直す運用も選べることです。予定は移動・招待更新・再作成などで状態が変わるため、「昔のIDを永遠に信じる」より、「検索で現物に当てる」方が強い場面があります。
補足:アドイン側で取得したIDが EWS 形式なら、公式手段で Graph/REST 形式へ変換する
「検索で取る」方法が最も安全ですが、アーキテクチャ上どうしても “ID直参照” をしたい場面もあります。その場合は、IDを力技で置換するのではなく、公式のID変換APIを検討します。
Office.js:convertToRestId で URL-safe なIDに変換する考え方
Outlook アドインで取得できる itemId が EWS 形式で、URL 非安全文字を含み得ることは、Office アドイン向けドキュメントでも注意点として扱われています。そこで、アドイン側で convertToRestId により REST 形式へ変換し、バックエンドには “変換後ID” を渡す設計が取りやすくなります。
// 擬似コード(概念の例)
const ewsId = Office.context.mailbox.item.itemId;
// Outlook REST / Graph で扱いやすい形式に変換する発想
const restId = Office.context.mailbox.convertToRestId(
ewsId,
Office.MailboxEnums.RestVersion.v2_0
);
// restId をDBに保存、以後は restId をベースに突合
重要なのは「このIDは Graph の event.id そのものではないかもしれない」という点です。ですが、少なくとも URL で壊れやすい文字が混ざるリスクを下げる目的では有効です。既に EWS ID を大量に保存してしまっている場合も、同様の発想で “正しい形式へ寄せる” ことができます。
Microsoft Graph:translateExchangeIds で ID 形式を相互変換する
Graph には Exchange 系IDと REST/Graph 系IDを相互に変換するための translateExchangeIds が用意されています。バックエンドで EWS ID を受け取ってしまう構成でも、Graph に変換させることで後段の処理を安定させられます。
POST https://graph.microsoft.com/v1.0/me/translateExchangeIds
Content-Type: application/json
{
"inputIds": [
"{ewsId_1}",
"{ewsId_2}"
],
"sourceIdType": "ewsId",
"targetIdType": "restId"
}
変換後に返ってきたIDを “Graph 呼び出し用のID” として扱えば、/me/events/{id} の事故(スラッシュ問題を含む)を回避しやすくなります。加えて、移行期に「古いDBには EWS ID、新しい処理は Graph」を混在させる場合も、変換を挟むことで設計をシンプルにできます。
| やりたいこと | おすすめ手段 | 強み | 注意点 |
|---|---|---|---|
| 「/」を含むIDでも確実に対象を見つけたい | iCalUId 検索 | URLパス問題を根絶、追加条件で安定運用しやすい | 繰り返し予定では複数ヒットし得る |
| 既に EWS ID を保存してしまった | translateExchangeIds で変換し直す | 既存資産を活かして段階移行できる | 変換API呼び出し分のコストが増える |
| アドインで取得したIDをバックエンドに渡す | アドイン側で convertToRestId | バックエンド実装を簡素化 | どの形式に変換して保存するかをチームで統一 |
「/ を – に置換」はなぜ危険なのか(そして、どうして動いてしまうことがあるのか)
スレッドの結末として「/ を - に置換したら動いた」という話が出ることがあります。たしかに短期的には動くかもしれませんが、運用としてはおすすめしません。理由は大きく3つです。
- ID衝突のリスク:元のIDに
-が含まれる可能性があり、単純置換は可逆ではありません。別のIDが同じ文字列に化ける余地が残ります。 - 将来の仕様変更に弱い:IDは opaque(中身の意味を想定しない)であり、文字種のルールが変わってもおかしくありません。置換ルールが永遠に正しい保証はありません。
- 障害調査が難しくなる:置換後の値が「本物のIDではない」状態になるため、ログやサポート問い合わせで “そのIDはどの形式か” の切り分けが一気に複雑になります。
もし “置換で動く” 状況があるなら、それは多くの場合、本来使うべき形式(URL-safe なID)に近い表現へ偶然寄っているだけです。再現性のある設計にするなら、iCalUId 検索か、convertToRestId/translateExchangeIds のような公式の変換に寄せるのが堅実です。
実務で困らないためのデータ設計:DBには「id だけ」ではなく複数のキーを持たせる
予定連携は、ユーザー操作(移動/キャンセル/再作成)やシリーズ展開(繰り返し)などで状態が揺れます。DBには event.id だけを保存して終わり、にすると後から詰みやすいので、次のように “復元に使える情報” をセットで持つ設計がおすすめです。
| カラム例 | 型の例 | 保存理由 | 更新タイミング |
|---|---|---|---|
| graph_event_id | 文字列 | Graphでの直接操作に使う第一キー | Graphから取得できた時点で更新 |
| ical_uid | 文字列 | IDが壊れた/形式不明でも検索で復元できる | アドイン取得時、またはGraph取得時 |
| start_utc | 日時 | 繰り返し予定の突合、複数ヒット時の決め手 | 予定変更を検知したら更新 |
| end_utc | 日時 | 突合の補助、表示用 | 同上 |
| organizer_address | 文字列 | 同名・同時間帯の取り違え防止 | 初回保存時 |
| series_master_id | 文字列 | 繰り返しシリーズの親を辿るため | Graphで取得できた時点で更新 |
これにより「普段は graph_event_id で一発」「壊れたら ical_uid + start_utc で復元」の二段構えが作れます。復旧ロジックを先に設計しておくと、障害時の対応が圧倒的に楽になります。
おすすめの処理フロー:取得・更新・削除を安全に回す手順
| 処理 | 最初に使うキー | ダメだった時のフォールバック | 最終的に使うAPI |
|---|---|---|---|
| 詳細表示(GET) | graph_event_id | ical_uid で検索して graph_event_id を再取得 | GET /me/events/{id} |
| 更新(PATCH) | graph_event_id | ical_uid + start_utc で再特定 → id 更新 | PATCH /me/events/{id} |
| 削除(DELETE) | graph_event_id | ical_uid で検索 → 正しい1件を確定 → id で削除 | DELETE /me/events/{id} |
このフローにすると、「スラッシュが入ってパスに載らない」「保存していたIDが実は EWS 形式だった」「繰り返しで候補が複数返る」などの問題を 段階的に吸収できます。
トラブルシューティング:よくあるハマりどころとチェック項目
| 症状 | ありがちな原因 | 確認ポイント | 対処 |
|---|---|---|---|
/me/events/{id} が 4xx で失敗する | ID内の / がパス区切りとして解釈された | リクエストURLが途中で分断されていないか | iCalUId 検索に切り替える、またはID形式を変換する |
%2F にしてもダメ | エンコードされたスラッシュを許可しない経路がある | アプリのルーティング/プロキシ設定、APIゲートウェイ | 「パスに載せない」設計へ |
$filter=id eq '...' が通らない | Graph のイベントで id はフィルター対象として想定されていない | フィルター可能プロパティの一覧 | iCalUId を使う |
iCalUId 検索で複数返る | 繰り返し/例外の影響 | type、seriesMasterId、開始日時 | 開始日時やシリーズ情報で1件に絞り込む |
まとめ:スラッシュ問題は「URLエンコード」より「検索・変換」で解くのが近道
Outlook のイベントIDにスラッシュが含まれて Graph の GET /me/events/{event-id} が失敗する問題は、URL パスの性質上、根性で回避しづらい類のトラブルです。実務的には次の優先順位で対策するのが安全です。
- 最優先:
iCalUIdで検索してイベントを見つけ、Graph が返すidを使う - 更新/削除まで含める:「検索 → id 取得 → id で操作」の2段階運用にする
- ID形式のズレがある:
convertToRestId/translateExchangeIdsなど公式の変換で統一する - 避けたい:文字の置換でごまかす(衝突・調査難・将来耐性の面で不利)
「IDはopaqueで、中身を推測しない」が Graph 連携の鉄則です。URLパスに載らないなら、パスに載せない。形式が違うなら、公式に変換する。これだけで、同じ問題に二度悩まされる確率を大きく下げられます。

コメント