Azure App Service で Microsoft Graph SendMail が “Specified HTTP method is not allowed for the request target”(HTTP 405)になる原因と対処徹底ガイド

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://&lt;app-name&gt;.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&lt;HttpResponseMessage&gt; SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
{
    Console.WriteLine($"[Graph] &gt;&gt; {request.Method} {request.RequestUri}");
    if (request.Content != null)
    {
        var reqBody = await request.Content.ReadAsStringAsync(cancellationToken);
        Console.WriteLine($"[Graph] &gt;&gt; Body: {reqBody}");
    }

    var response = await base.SendAsync(request, cancellationToken);
    Console.WriteLine($"[Graph] &lt;&lt; {(int)response.StatusCode} {response.ReasonPhrase}");
    if (response.Headers.TryGetValues("Allow", out var allow))
    {
        Console.WriteLine($"[Graph] &lt;&lt; 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 向けの発呼結果(成功/失敗/所要時間)を継続監視。

ミスしやすい実装ポイント(チェックリスト)

  1. エンドポイント:/v1.0/users/{id}/sendMail を POST しているか(App-only)。
  2. スコープ:.default を指定して app-only トークンを取っているか。
  3. メールボックス:送信元ユーザーに有効な Exchange Online メールボックスがあるか。
  4. アプリ設定:クライアント シークレット名/値、スロット設定、環境変数のスペル。
  5. ネットワーク:VNET 統合のルート、NVA/Firewall のメソッド制御、Outbound 443 の可否。
  6. ログ:raw HTTP(Method/URL/Status/Allow ヘッダー)を取得できているか。
  7. 切り分け: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成功——
400Bad RequestJSON 体裁誤り、アドレス不正リクエスト本文を検証
401Unauthorizedトークン不正・失効トークン取得・スコープを確認
403Forbidden権限不足(Mail.Send 未同意 等)権限付与と管理者同意
404Not Foundユーザーが存在しないUPN/ID を確認
405Method Not Allowedメソッド/URL 不一致、NW 書き換えraw HTTP で Method/Allow を確認
415Unsupported Media TypeContent-Type 不正application/json を指定
429Too Many RequestsスロットリングRetry-After に従い再試行

まとめ

HTTP 405 は「権限」よりもまず「メソッドと URL の整合」が焦点です。SDK の有無に関わらず、最初に raw HTTP を確かめ、次に 送信者のメールボックス、アプリ権限、App Service のネットワーク経路を順に詰める――この順路が、ローカルでは成功・App Service でのみ失敗といった厄介な事象の切り分けを最短化します。最終的な解決として、VNET/UDR/Firewall とアプリ設定(特に WEBSITE_VNET_ROUTE_ALL と秘密情報管理)を見直せば、再発を防ぎつつ安定運用につなげられます。

この記事を書いた人

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

コメント

コメントする

目次