ASP.NET Core Minimal APIで/webhookにセッションやクッキーを設定したのに、別エンドポイントで値が引き継がれずnullになる…。この手のトラブルは「そのリクエストを叩いているクライアントが誰か」「クッキーがブラウザーに保存されているか」「Webhookはそもそもセッション前提ではないか」を押さえるだけで一気に解決します。再現確認の方法から、Cognito FormsのJSON Webhook受信までまとめて解説します。
よくある状況:/webhookでセットしたはずなのに次のページでnull
Minimal API(Program.cs 1ファイル構成)で /webhook を作り、受信時に次のような処理をしているケースを想定します。
context.Session.SetString("YourSessionKey", "YourSessionValue")でセッションに値を保存Response.Cookies.Append("myKey", "myValue", options)でクッキーを発行
ところが、同じアプリ内の別エンドポイント(例:/status や /another-page)で Session.GetString をすると毎回null。Postmanではレスポンスヘッダーに Set-Cookie が見えるのに、ブラウザーの開発者ツールではクッキーが見えず、当然セッションも続かない…という状態です。
結論:セッションは「セッションID入りクッキー」を返してもらって成立する
ASP.NET Coreのセッションは、サーバー側に値を保持する仕組みである一方、「どのセッションか」を識別するためのセッションIDはクッキーでやり取りします。流れはシンプルです。
| タイミング | サーバー側で起きること | クライアント側で必要なこと |
|---|---|---|
| 初回リクエスト | セッションIDを生成し、レスポンスにSet-Cookieで返す(例:.AspNetCore.Session) | 受け取ったクッキーを保存する |
| 2回目以降 | リクエストのCookieからセッションIDを読み、同じセッション領域を参照する | リクエストヘッダーに同じクッキーを付けて送る |
つまり、次のリクエストでセッション値を読みたいなら「次のリクエストを送るクライアントが、前回のセッション用クッキーを持っている」必要があります。ここが崩れると、サーバー側では毎回「別セッション」と判断され、GetStringはnullになります。
「Postmanでは見えるのにブラウザーでは見えない」最大の理由
原因の筆頭はこれです。
Postman(または外部サービス)が/webhookを叩いているだけで、ブラウザーは/webhookに一度もアクセスしていない。
この場合、/webhookのレスポンスに含まれるCookieは「Postmanの中」には保存されますが、ブラウザーには届きません。ブラウザーが受け取っていない以上、開発者ツールに出ることもありませんし、別エンドポイントにアクセスしてもCookieが付かないのでセッションは引き継がれません。
もう一段深掘り:クッキーが保存されない/送られない典型パターン
「/webhookをブラウザーから叩いたつもりなのに、やっぱりクッキーが見えない」場合は、次のズレを疑うのが近道です。
| 症状 | よくある原因 | チェック/対策 |
|---|---|---|
| 開発者ツールにクッキーが出ない | Secure属性付きなのにHTTPでアクセスしている | ローカルはHTTPSで確認。HTTPならSecure=false(本番はHTTPS前提) |
| 別エンドポイントでクッキーが送られない | CookieのPathが狭い(例:/webhookに限定) | アプリ全体で使うならPath="/"にする |
| 同じアプリなのにセッションが続かない | localhostと127.0.0.1など、アクセスしているホスト名が違う | ブラウザーとPostman(外部サービス)で同一ホストに統一する |
| JavaScriptのfetchで呼ぶとセッションが続かない | 別オリジン呼び出しでCookieが送られていない(CORS/credentials/SameSite) | credentials: 'include'、サーバー側CORSでAllowCredentials、CookieはSameSite=None+Secure |
特にSameSite=Noneを付ける場合、CookieはSecureも必須という仕様になっています。クロスサイトでCookieを使う設計にするなら、この組み合わせは避けて通れません。
Minimal APIでのセッション設定:Program.csの最小テンプレ
Minimal APIでセッションを使うときは、基本的に「キャッシュ(セッションストア)」「セッション登録」「セッションミドルウェア」をセットで用意します。以下はローカル検証向けのインメモリ構成です(複数台構成や再起動耐性が必要ならRedisなどに置き換えます)。
using Microsoft.AspNetCore.Http;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
// 1) セッションの保存先(まずはメモリでOK)
builder.Services.AddDistributedMemoryCache();
// 2) セッションの登録
builder.Services.AddSession(options =>
{
// Cookie名はわかりやすく変えておくとデバッグが楽です
options.Cookie.Name = ".MyApp.Session";
options.IdleTimeout = TimeSpan.FromMinutes(20);
// セキュリティと同意ポリシー
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
// クロスサイト要件がないなら Lax が無難(Noneを使うとSecure必須)
options.Cookie.SameSite = SameSiteMode.Lax;
// HTTPSならSecure、HTTPなら非Secure(ローカルHTTP検証が必要な場合の保険)
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
var app = builder.Build();
app.UseHttpsRedirection();
// 3) セッションミドルウェア(エンドポイント処理より前に)
app.UseSession();
// 動作確認用
app.MapGet("/", () => "OK");
app.Run();
Minimal APIでは、エンドポイントがある場合にUseRoutingやUseEndpointsが自動で追加されるなど、パイプラインが一定のルールで組み立てられます。ミドルウェアの追加順序が結果に影響するため、セッションのように「リクエスト前後で処理が必要」なものは早めに登録しておくのが安全です。
ブラウザーで確実に確認する:セッション初期化用のGETを用意する
Webhookは基本的にPOSTで、ブラウザーのURL直打ち検証と相性が悪いです。そこで、原因切り分け用に「ブラウザーが確実に叩くGET」を一つ用意すると、話が早くなります。
app.MapGet("/init-session", (HttpContext context) =>
{
// セッションに値を入れる(ここでセッションCookieが発行される)
context.Session.SetString("YourSessionKey", "YourSessionValue");
// 独自Cookieも同時に発行して挙動を見やすくする
context.Response.Cookies.Append("myKey", "myValue", new CookieOptions
{
Path = "/",
Expires = DateTimeOffset.UtcNow.AddDays(7),
HttpOnly = true,
Secure = context.Request.IsHttps,
SameSite = SameSiteMode.Lax
});
return Results.Text("Session/Cookie initialized.");
});
app.MapGet("/check-session", (HttpContext context) =>
{
var sessionValue = context.Session.GetString("YourSessionKey");
var myCookie = context.Request.Cookies["myKey"];
return Results.Json(new
{
sessionValue,
myCookie,
hasSessionCookie = context.Request.Cookies.ContainsKey(".MyApp.Session")
});
});
確認手順は次の通りです。
- ブラウザーで
/init-sessionにアクセス - 開発者ツール(Application/Storage → Cookies)で
.MyApp.SessionとmyKeyが保存されているか確認 - 同じタブで
/check-sessionにアクセスして、sessionValueが返るか確認
この時点でセッションが引き継がれないなら、Cookie属性(Secure/SameSite/Path/ホスト名)の問題が濃厚です。逆にここが通るなら、「/webhookを叩いているのがブラウザーではない」という本質に戻って設計を見直すべきです。
/webhookでセッションを使いたくなる気持ちと、現実的な落としどころ
「Webhookが届いたことを、次のページで確認したい」目的でセッションに書きたくなることがあります。しかしWebhookはサーバー→サーバーの通知であり、送信元は基本的に“ブラウザーのようにクッキーを保持して再送”しません。つまり、Webhookでセットしたセッションを、あなたのブラウザーが後から読む、という発想が噛み合いません。
そこで、Webhook受信の「事実」を残す方法は、セッションではなくサーバー側ストレージに寄せるのが定石です。
| やりたいこと | セッション/クッキーでやると | おすすめの実装 |
|---|---|---|
| Webhookが届いたか確認したい | 送信元がCookieを持たないため、確認できない/不安定 | DBに受信ログを保存し、管理画面で表示 |
| 重複通知を防ぎたい | セッションは送信元単位にならず意味が薄い | payloadのIDや日時をキーに冪等(idempotent)に処理 |
| 失敗時に再試行したい | セッションではリトライ制御ができない | キューに積んで非同期処理、失敗時にリトライ |
Postmanで「同じセッション」を再現する方法
Postmanで複数回叩いて「前回セットしたセッションが次回も読める」状態を作りたい場合は、ブラウザーと同じくCookieの持ち回りが必要です。
- 1回目のレスポンスに含まれる
Set-Cookie(セッションID)を、2回目以降のリクエストでも送る - PostmanのCookie管理(Cookie Jar)を使うと、自動で保持・送信してくれる
ここで保持できているのは「Postmanのセッション」であり、ブラウザーのセッションとは別物です。ブラウザーでセッションを読みたいなら、ブラウザー自身にCookieを配布する必要があります。
WebhookのJSONをMinimal APIで受け取る基本
外部サービス(例:Cognito Forms)がWebhookとしてJSONをPOSTしてくる場合、Minimal API側は「JSONをボディから受け取る」形にします。Cognito Formsは設定で「Post JSON Data to a Website」を有効化し、エントリー作成/更新/削除ごとに指定URLへJSONをPOSTできます。
まずは“受信できているか”を最短で確認する
最初はモデルを固める前に、生のJSONをそのまま受け取ってログに残すのが安全です。フォーム側のフィールド構成が変わるとJSONも変わりやすいからです。
using System.Text.Json;
app.MapPost("/webhook", async (HttpContext context, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("Webhook");
// 受信JSONを生でパース(モデルが未確定でも壊れにくい)
using var doc = await JsonDocument.ParseAsync(context.Request.Body);
// 例:全体を文字列化してログ(本番では個人情報に注意)
logger.LogInformation("Webhook received: {Json}", doc.RootElement.ToString());
return Results.Ok(new { ok = true });
});
受信確認だけならこれで十分です。次に「必要な項目だけ取り出す」「DBに保存する」「重複を弾く」へ進めます。
項目がある程度わかってきたら、型で受ける
フィールド名が固定できるなら、C#クラス(またはrecord)を作ってそのまま受けられます。Minimal APIは複合型パラメータをボディからバインドするため、(Payload payload)の形が最短です。
public sealed class CognitoWebhookPayload
{
// 例:ドキュメントの例に合わせたプロパティ(実際のキーに合わせて調整)
public string? id { get; set; }
public string? email { get; set; }
public string? name { get; set; }
public DateTimeOffset? date_created { get; set; }
}
app.MapPost("/webhook-typed", (CognitoWebhookPayload payload, ILoggerFactory loggerFactory) =>
{
var logger = loggerFactory.CreateLogger("Webhook");
logger.LogInformation("Entry id={Id}, email={Email}", payload.id, payload.email);
return Results.Ok("OK");
});
ただし、フォームはフィールド追加・名称変更が起きがちです。その場合は、未知プロパティを吸収できる設計が強いです。
フィールドが可変なら JsonExtensionData で“全部受ける”
Cognito FormsはDeveloper ModeでJSONのキー名(JSON Names)をカスタマイズできます。フォームのラベルが日本語やスペースを含む場合でも、キー名をAPI向けに整えることが可能です。
それでもフィールドが増減する可能性があるなら、[JsonExtensionData]で「未知プロパティを辞書に集める」方式が便利です。
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class CognitoWebhookFlexiblePayload
{
// 代表的な固定項目だけ先に定義
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("date_created")]
public DateTimeOffset? DateCreated { get; set; }
// それ以外のフィールドはすべてここへ
[JsonExtensionData]
public Dictionary<string, JsonElement>? Fields { get; set; }
}
app.MapPost("/webhook-flex", (CognitoWebhookFlexiblePayload payload) =>
{
// 例:任意フィールドを取り出す
if (payload.Fields is not null && payload.Fields.TryGetValue("CompanyName", out var company))
{
var companyName = company.GetString();
// DB保存など
}
return Results.Ok();
});
この方法なら「新しいフィールドが増えたら即エラーで落ちる」を避けられます。Webhook受信は止まると業務影響が出やすいので、受信側は“壊れにくい”ほうが運用が楽です。
Webhook運用で重要:リトライされる前提で作る
Webhookはネットワーク状況によって失敗することがあります。Cognito Formsの場合、4xx/5xxを返すと最大15回・72時間にわたり再送するリトライ機構があります(ただし404/410/413はリトライ対象外)。
この前提を踏まえて、受信側は次の2点を必ず意識します。
- 冪等性(idempotency):同じ通知が複数回届いても二重登録しない(IDや送信日時をキーに重複排除)
- 素早く200を返す:重い処理はキュー/バックグラウンドに逃がし、受信自体は短時間で成功させる
ファイルURLが含まれる場合の注意点
Cognito FormsのWebhookで送られるファイルリンク(アップロードや生成ドキュメント)は有効期限が短く、概ね30分程度で無効になると案内されています。ファイルが必要なら、受信後すぐにダウンロードしてストレージへ退避するフローにしておくと安全です。
「受信したこと」をブラウザーで見たい場合の実装例
Webhook受信を管理画面で可視化したいなら、セッションではなくDB(または一時的にメモリ)へ“受信ログ”を保存し、ブラウザーはそれを読むだけにします。最小構成のイメージは次の通りです。
// 例:最小の受信ログ(本番はDBに置き換え推奨)
var received = new List<(DateTimeOffset at, string raw)>();
app.MapPost("/webhook", async (HttpContext ctx) =>
{
using var doc = await JsonDocument.ParseAsync(ctx.Request.Body);
received.Add((DateTimeOffset.UtcNow, doc.RootElement.ToString()));
return Results.Ok();
});
app.MapGet("/webhook-status", () =>
{
var last = received.LastOrDefault();
return Results.Json(new
{
count = received.Count,
lastReceivedAtUtc = last.at == default ? null : last.at,
lastPayloadPreview = string.IsNullOrEmpty(last.raw) ? null : last.raw[..Math.Min(200, last.raw.Length)]
});
});
ポイントは「Webhook受信」と「ブラウザー表示」を完全に分けることです。これならWebhookがどこから来ても記録され、ブラウザーは常に同じ場所(DB/ログ)を見に行けばよくなります。
セッション/クッキーを使うべき場面・使わないほうがいい場面
| 場面 | セッション/クッキー | おすすめ |
|---|---|---|
| 同一ブラウザーでの画面遷移(ログイン、ウィザード、カート) | 相性が良い | セッション/認証Cookieを使う |
| Webhookなどサーバー間通知 | 相性が悪い | DB保存・冪等処理・署名検証など“ステートレス寄り”に |
| SPAが別ドメインAPIを叩く | 設定が難しい(SameSite/CORS/CSRF) | 可能ならトークン認証(JWT等)。Cookie運用なら要件を明確化 |
トラブルシューティングチェックリスト
- /webhookを叩いているのは誰か:ブラウザーか、Postmanか、外部サービスか
- クッキーはどこに保存されたか:PostmanのCookie Jarか、ブラウザーのCookieストレージか
- ホスト名は一致しているか:
localhostと127.0.0.1の混在、サブドメイン違い - Secure/SameSite/Path:HTTPでSecureを付けていないか、Pathが狭すぎないか
- Webhookはリトライされる:二重処理にならない設計か、200を返せているか
まとめ
- セッションが別エンドポイントでnullになる多くの原因は、次のリクエストで同じセッションCookieが返ってきていないこと
- Postmanで見えるCookieはPostman内に保存されるだけで、ブラウザーが/webhookを叩いていなければブラウザーには配布されない
- Webhookはセッション前提ではないため、受信確認や重複排除はDB保存+冪等設計に寄せる
- Cognito FormsのWebhookはJSONをPOSTでき、リトライやファイルURLの期限など運用上の注意点があるため、受信側は“壊れにくい”実装が重要

コメント