Azure App Service で Microsoft Graph の SendMail が “Specified HTTP method is not allowed for the request target” (HTTP 405)になるときの原因と解決策まとめ
ローカル(開発機)では GraphServiceClient によるメール送信が成功するのに、Azure App Service に配置すると HTTP 405(Specified HTTP method is not allowed for the request target)で失敗する――。本稿はこの現象を体系的に切り分け、最短で解決へ導くための実践ガイドです。SDK の使い方、Graph の権限設計、メールボックス要件、App Service のネットワーク経路、ログの取り方まで、現場でつまずきがちなポイントを具体的な手順とコードで整理しました。
質問概要(症状の再掲と整理)
- .NET(C#)アプリで
GraphServiceClientを使い、Users["[email protected]"].SendMail.PostAsync(body)でメール送信。 - ローカル実行では成功するが、Azure App Service へデプロイすると HTTP 405 が返却され送信できない。
- アプリ登録には Mail.Send(Application) 権限を付与済みで、グローバル管理者の同意も完了。
- クライアント ID/テナント ID/クライアント シークレットはローカルと同一。
- App Service は VNET 統合あり(FW を無効化しても症状は変わらず)。
いわゆる「ローカルでは動くのに本番で 405」という状況は、HTTP メソッド/URL/認証/ネットワーク経路のいずれかに差異があるときに起きがちです。以下の順で確認・是正していくと、原因の切り分けが最短になります。
まず押さえる:HTTP 405 の意味と Graph での典型パターン
HTTP 405 は「そのリソースに対して、今の HTTP メソッド(GET/POST/…)は許可されていない」という意味です。Microsoft Graph の場合、たとえば /sendMail エンドポイントは POST のみを受け付けます。もし「意図せず GET になっている」「/me/sendMail を叩いている」「余計なプロキシや NVA がリクエストを書き換えている」等が起きると 405 が返り得ます。
<figure>
<figcaption>HTTP 405 が返るときに疑うポイント(早見表)</figcaption>
<table>
<thead>
<tr>
<th>現象</th>
<th>ありがちな原因</th>
<th>確認方法</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td>405 Method Not Allowed</td>
<td>メソッドが POST 以外/エンドポイント誤り(<code>/me/sendMail</code> など)</td>
<td>SDK の <em>raw HTTP</em> ログで <code>Method</code> と <code>RequestUri</code> を確認</td>
<td>正しい <code>POST /v1.0/users/{userId}/sendMail</code> へ修正</td>
</tr>
<tr>
<td>405 とともに <code>Allow</code> ヘッダーが返る</td>
<td>対象リソースが <code>GET</code> など別メソッドのみ許可</td>
<td>レスポンス ヘッダー <code>Allow</code> を記録</td>
<td>エンドポイントの取り違えを修正</td>
</tr>
<tr>
<td>ローカル OK/App Service NG</td>
<td>VNET 経路で NVA/Firewall による書き換え・ブロック</td>
<td>Kudu で <code>tcpping graph.microsoft.com 443</code>、依存関係ログ</td>
<td>Outbound 443 を許可、ルート/NSG を見直し</td>
</tr>
<tr>
<td>401/403 に見えるが 405</td>
<td>トークンは通るがメソッド不一致</td>
<td>トレースで <code>WWW-Authenticate</code> と <code>Allow</code> を確認</td>
<td>呼び出し方法の整合性を再点検</td>
</tr>
</tbody>
</table>
</figure>
解決への手順 1:エンドポイントと HTTP メソッドを 100% 確認する
アプリケーション権限(Application permissions)でメール送信する場合、必ず以下のエンドポイント・メソッドを使います。
POST https://graph.microsoft.com/v1.0/users/{userId or userPrincipalName}/sendMail
Content-Type: application/json
Authorization: Bearer <app-only token>
SDK 経由でも内部的にはこの HTTP リクエストが発行されます。まずは SDK の前提を取り払い、生の HTTP を記録して「POST になっているか」「/users/{id}/sendMail か」を目視で検証しましょう。
(.NET 6+/Graph SDK v5)最小コード例:Application 権限で sendMail
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Graph.Users.Item.SendMail;
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var scopes = new[] { "[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default)" };
// v5 以降は TokenCredential を直接渡せます
var graph = new GraphServiceClient(credential, scopes);
var msg = new Message {
Subject = "App Service からのテスト送信",
Body = new ItemBody { ContentType = BodyType.Html, Content = "こんにちは。テストです。" },
ToRecipients = new List {
new Recipient { EmailAddress = new EmailAddress { Address = "[[email protected]](mailto:[email protected])" } }
}
};
var body = new SendMailPostRequestBody {
Message = msg,
SaveToSentItems = true
};
// Application 権限では /users/{UPN or Id}/sendMail を使う
await graph.Users["[[email protected]](mailto:[email protected])"].SendMail.PostAsync(body);
上記の Users["..."].SendMail.PostAsync() が内部で POST /v1.0/users/{id}/sendMail を叩いていることを必ずログで確認します。これが /me/sendMail になっている、あるいはメソッドが GET に置き換わっていると 405 になります。
解決への手順 2:Graph の権限(Application)を再点検する
- Mail.Send(Application) がアプリ登録に付与され、管理者による同意が完了していること。
- App-only では スコープは
.defaultを使用します(例:https://graph.microsoft.com/.default)。 - クライアント シークレットの有効期限切れ/設定名のタイプミス/スロット間の値ずれに注意。
権限とエンドポイントの対応
| 権限の種類 | 想定エンドポイント | 誰の名義で送信? | よくある誤り |
|---|---|---|---|
| Delegated(委任) | /me/sendMail | サインイン中のユーザー | App-only なのに /me/sendMail を呼ぶ |
| Application(アプリのみ) | /users/{id}/sendMail | 指定ユーザー(サービスアカウント等) | 権限はあるがメールボックスが存在しない |
解決への手順 3:送信元ユーザーに 有効な Exchange Online メールボックス があるか
Application 権限では委任は不要ですが、実在するユーザーのメールボックスが必要です。ライセンス未割当/共有メールボックス化/無効化済みなどの場合、送信に失敗します。簡易チェックとしては以下が有効です。
- Exchange Online PowerShell:
Connect-ExchangeOnline→Get-Mailbox -Identity [email protected] - Graph:
GET /v1.0/users/{id}でmail/userPrincipalNameを確認
解決への手順 4:App Service のネットワーク経路を調べる(VNET 統合/NAT/NVA/NSG)
App Service で VNET 統合を有効にしていると、設定により パブリック宛でも VNET 経由で外部へ出ていきます。途中に NVA(仮想アプライアンス)や Firewall があると、POST がブロック/書き換えされ、Graph 側からは「そのメソッドは許可されていない」(=405)に見える場合があります。
WEBSITE_VNET_ROUTE_ALL の挙動
WEBSITE_VNET_ROUTE_ALL=1:すべてのアウトバウンドを VNET 統合側へルーティング(NAT Gateway や NVA 経由)。WEBSITE_VNET_ROUTE_ALLを未設定/0:RFC1918 等のプライベートアドレスのみ VNET へ。インターネット宛は App Service 既定の SNAT を使用。
Firewall/NVA による HTTP メソッド制御、もしくは誤った UDR(ユーザー定義ルート)でプロキシを経由していると、ローカルでは成功・App Service では 405 といった差異が生まれます。
<figure>
<figcaption>ネットワーク観点のチェックポイント</figcaption>
<table>
<thead>
<tr>
<th>項目</th>
<th>確認ポイント</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td>VNET 統合とルート</td>
<td>Route Table(UDR)で 0.0.0.0/0 が NVA/Firewall に向いていないか</td>
<td>必要に応じて <code>WEBSITE_VNET_ROUTE_ALL</code> を無効化/UDR を調整</td>
</tr>
<tr>
<td>Outbound 送信元 IP</td>
<td>SNAT/NAT GW の送信元が Graph にブロックされていないか</td>
<td>NAT GW の固定化、Firewall 側で許可</td>
</tr>
<tr>
<td>NSG/Firewall ポリシー</td>
<td>TCP 443 の <strong>POST</strong> を含む双方向セッションが許可されるか</td>
<td>Outbound 443 を許可。<em>注</em>:NSG は FQDN 指定不可のため、FQDN 制御は Azure Firewall 等で実施</td>
</tr>
<tr>
<td>プロキシ/NVA</td>
<td>HTTP メソッド/ヘッダーを書き換えていないか</td>
<td>バイパス設定を追加、あるいは直接インターネット経路に切替</td>
</tr>
</tbody>
</table>
</figure>
<h3>Kudu/SSH での簡易疎通テスト</h3>
# Kudu(https://<app-name>.scm.azurewebsites.net)コンソールで
```
tcpping graph.microsoft.com 443
# 依存関係の観点ではアプリから実際に HTTP を投げて確認するのが確実
```
解決への手順 5:raw HTTP を見る(SDK を使っていてもここが最短)
SDK を疑う前に、「実際にどんな HTTP が出ているか」を確定させます。Graph .NET SDK v5 では DelegatingHandler を差し込んで簡単にログが取れます。
using System.Net.Http.Headers;
using Microsoft.Graph;
using Microsoft.Kiota.Http.HttpClientLibrary; // HttpClientRequestAdapter
using Microsoft.Kiota.Abstractions;
public sealed class LoggingHandler : DelegatingHandler
{
public LoggingHandler(HttpMessageHandler inner = null)
: base(inner ?? new HttpClientHandler()) { }
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"[Graph] >> {request.Method} {request.RequestUri}");
if (request.Content != null)
{
var reqBody = await request.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"[Graph] >> Body: {reqBody}");
}
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine($"[Graph] << {(int)response.StatusCode} {response.ReasonPhrase}");
if (response.Headers.TryGetValues("Allow", out var allow))
{
Console.WriteLine($"[Graph] << Allow: {string.Join(",", allow)}");
}
return response;
}
}
// ハンドラーを組み込んだ Graph クライアントの作成例
var handlers = new DelegatingHandler[] { new LoggingHandler() };
var httpClient = GraphClientFactory.Create(handlers);
var adapter = new HttpClientRequestAdapter(credential, scopes: scopes, httpClient: httpClient);
var graph = new GraphServiceClient(adapter);
このログで Method が POST、URL が /v1.0/users/…/sendMail になっているか、レスポンスの Allow がどう返っているかを確認します。Allow: GET, HEAD のように返る場合は、意図しない別リソースに当たっています。
解決への手順 6:切り分けテストで「App Service 固有」かを判断する
- 同じコードを IIS VM や Azure Functions にデプロイし、再現するか確認。
- ネットワークを完全に切り離したVNET 非統合の App Service(新規)にも配置してみる。
- Azure CLI の
az restで SDK 非依存に Graph へ直接 POST してみる。
# Azure CLI で MS Graph のアクセストークンを取得
TOKEN=$(az account get-access-token --resource-type ms-graph --query accessToken -o tsv)
# /users/{id}/sendMail へ直接 POST(簡易検証)
cat <<'JSON' > mail.json
{
"message": {
"subject": "CLI 経由のテスト送信",
"body": { "contentType": "Text", "content": "Hello from az rest" },
"toRecipients": [ { "emailAddress": { "address": "[[email protected]](mailto:[email protected])" } } ]
},
"saveToSentItems": true
}
JSON
curl -sS -D -
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-X POST "[https://graph.microsoft.com/v1.0/users/[email protected]/sendMail](https://graph.microsoft.com/v1.0/users/[email protected]/sendMail)"
--data-binary @mail.json
これで成功するなら、コード/認証よりも App Service のネットワーク(VNET/UDR/NVA/Firewall) が疑わしいと判断できます。
最終的な解決(実例)
実際の現場報告では、VNET/ネットワーク設定とアプリ設定の両方を調整することで解決に至っています。たとえば次のような構成見直しが奏功しました。
- VNET サブネットの NSG/Firewall 側で TCP 443 Outbound を許可(補足:NSG は FQDN 指定不可。FQDN ベースの許可が必要な場合は Azure Firewall/NVA で
graph.microsoft.comを許可)。 - App Service の
WEBSITE_VNET_ROUTE_ALLを無効化し、インターネット宛は既定 SNAT を使用(NVA 経由の書き換え影響を回避)。 - スロット間でズレていたアプリ設定(クライアント シークレット名のタイプミス等)を修正。
この「ネットワークと設定の両輪」アプローチにより、ローカルと App Service の実行経路が一致し、405 が解消されています。
ベストプラクティス:今後 405 を起こさないために
- SDK を使っていても raw HTTP を記録する:障害解析の初動はここが最短。
DelegatingHandlerを常設しておくと可観測性が上がります。 - VNET 統合時は経路を明示:パブリック宛でも VNET 経由になる場合があるため、UDR/Firewall/プロキシの設定に注意。
- シークレット管理の一元化:Azure Key Vault 参照を使い、スロット設定を有効化して本番/ステージングのズレをなくす。
- Managed Identity の活用:可能ならクライアント シークレットを廃し、システム割り当てマネージド ID に Graph の Application 権限(Mail.Send)を付与してトークンを取得(
DefaultAzureCredential)。秘密情報の同期ミスを根絶できます。 - 依存関係テレメトリ:Application Insights の Dependency ログを有効化し、
graph.microsoft.com向けの発呼結果(成功/失敗/所要時間)を継続監視。
ミスしやすい実装ポイント(チェックリスト)
- エンドポイント:
/v1.0/users/{id}/sendMailを POST しているか(App-only)。 - スコープ:
.defaultを指定して app-only トークンを取っているか。 - メールボックス:送信元ユーザーに有効な Exchange Online メールボックスがあるか。
- アプリ設定:クライアント シークレット名/値、スロット設定、環境変数のスペル。
- ネットワーク:VNET 統合のルート、NVA/Firewall のメソッド制御、Outbound 443 の可否。
- ログ:raw HTTP(Method/URL/Status/Allow ヘッダー)を取得できているか。
- 切り分け:VNET 非統合の新規 App/IIS VM/Functions/
az restでの再現性。
付録:コード断片(v4 以前をお使いの場合)
旧 API(v4 系)では IAuthenticationProvider と GraphServiceClient の組み合わせで構築します。移行中の参考に。
// v4 系の概念例(パッケージにより API は異なります)
var authProvider = new DelegateAuthenticationProvider(async (request) =>
{
var token = await confidentialClient.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }).ExecuteAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
});
var graphClient = new Microsoft.Graph.GraphServiceClient(authProvider);
// /users/{id}/sendMail を POST
await graphClient.Users["[[email protected]](mailto:[email protected])"].SendMail(
new Microsoft.Graph.Message
{
Subject = "v4 からの送信",
Body = new Microsoft.Graph.ItemBody { ContentType = Microsoft.Graph.BodyType.Text, Content = "test" },
ToRecipients = new[] { new Microsoft.Graph.Recipient { EmailAddress = new Microsoft.Graph.EmailAddress { Address = "[[email protected]](mailto:[email protected])" } } }
},
true
).Request().PostAsync();
付録:ステータスコード早見表
メール送信時の代表的なステータスコード
| コード | 意味 | 主因 | 対処の起点 |
|---|---|---|---|
| 200/202 | 成功 | — | — |
| 400 | Bad Request | JSON 体裁誤り、アドレス不正 | リクエスト本文を検証 |
| 401 | Unauthorized | トークン不正・失効 | トークン取得・スコープを確認 |
| 403 | Forbidden | 権限不足(Mail.Send 未同意 等) | 権限付与と管理者同意 |
| 404 | Not Found | ユーザーが存在しない | UPN/ID を確認 |
| 405 | Method Not Allowed | メソッド/URL 不一致、NW 書き換え | raw HTTP で Method/Allow を確認 |
| 415 | Unsupported Media Type | Content-Type 不正 | application/json を指定 |
| 429 | Too Many Requests | スロットリング | Retry-After に従い再試行 |
まとめ
HTTP 405 は「権限」よりもまず「メソッドと URL の整合」が焦点です。SDK の有無に関わらず、最初に raw HTTP を確かめ、次に 送信者のメールボックス、アプリ権限、App Service のネットワーク経路を順に詰める――この順路が、ローカルでは成功・App Service でのみ失敗といった厄介な事象の切り分けを最短化します。最終的な解決として、VNET/UDR/Firewall とアプリ設定(特に WEBSITE_VNET_ROUTE_ALL と秘密情報管理)を見直せば、再発を防ぎつつ安定運用につなげられます。

コメント