Skip to content

MS_HttpApplicationModuleHandler

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

HttpApplication(Global.asax)、HttpModule、HttpHandler

概要

HttpApplication(Global.asax)
HttpModuleHttpHandler等について。

補足(三者の関係を先に): 名前が似ていて混同されやすいので、
役割の違いを先に整理しておく。

【リクエストの流れ】

  ブラウザ
    ↓
  [ HttpApplication ]  … アプリ全体の共通処理(Global.asax)
    ↓
  [ HttpModule ]  [ HttpModule ]  [ HttpModule ] …
      ↑ 全リクエストが通る「フィルター」。何個でも並べられる
    ↓
  [ HttpHandler ]  … 実際に応答を作る「終点」。1 つだけ
      (.aspx → PageHandlerFactory、.ashx → 自作ハンドラー …)
    ↓
  [ HttpModule ](後処理)…
    ↓
  ブラウザ
個数 役割 対応する ASP.NET Core
HttpApplication 1(プール) アプリ全体のイベント Program.cs / ミドルウェア
HttpModule 何個でも 横断的な処理(認証、ログ、書き換え) ミドルウェア
HttpHandler 1 つだけ 応答の生成 エンドポイント(Map〜)

HttpModule = ミドルウェア、HttpHandler = エンドポイント
という対応で捉えると、ASP.NET Coreへの移行
見通しが良くなる。

HttpApplication(Global.asax)

  • 共通の処理を定義する。

    • メソッド
    • プロパティ
    • イベント
  • Global.asax ファイルで定義する。

  • ライフサイクル

HttpModule

要求を傍受、参加、または変更できる。

HttpHandler

ISAPI拡張機能に類似した機能。

  • 個々のエンドポイント要求を処理するために使用する。

  • 要求を処理するために使用されるハンドラーは 1 つのみ。

  • インターフェイス IHttpHandler を実装する。

  • アプリ内の HTTP URL または URL 拡張機能のグループを処理できる。

  • ライフサイクル

詳細

ライフサイクル

  • 同時実行数に合わせてインスタンス化される。

  • リクエスト-レスポンス間は専有する(メンバはスレッドセーフ)。

  • インスタンス化されたオブジェクトは複数ユーザ間で使いまわされる。

  • なお、HttpApplicationHttpModule
    アクセスできるイベントが異なる。

    • HttpApplication

      • アプリケーションレベルのイベント
      • リクエストレベルのイベント
    • HttpModule

      • リクエストレベルのイベント

補足(原文が言う「思わぬ落とし穴」の中身): この 3 行が
本ページで最も重要な記述である。

【誤解】
  HttpApplication / HttpModule は「リクエストごとに new される」
    → インスタンス変数に状態を持ってよい

【実際】
  プール(インスタンス プール)から取り出して使い回される
    ・1 リクエストの間は 1 スレッドが専有する(ここは安全)
    ・しかし、リクエストが終わるとプールに戻る
    ・次のリクエストで「別のユーザー」に貸し出される
      → 前のユーザーの値が残っていると情報漏洩
public class MyModule : IHttpModule
{
    private string _userId;   // ← 危険。使い回される

    public void Init(HttpApplication app)
    {
        app.AuthenticateRequest += (s, e) => { _userId = GetUser(); };
        app.EndRequest          += (s, e) => { Log(_userId); };
        //   → 同一リクエスト内なら動くが、
        //     クリアし忘れると次のリクエストに残る
    }
}

リクエスト単位の状態は HttpContext.Items に持つのが定石である。

HttpContext.Current.Items["UserId"] = userId;   // リクエスト終了で破棄される

Init は初回のみ呼ばれる(後述の★印)ため、
Init で確保したものは全リクエストで共有される点にも注意する。

ASP.NET Core のミドルウェアでも同じ罠がある
(ミドルウェアのインスタンスはアプリ起動時に 1 つだけ作られる)。

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    // ここにリクエスト固有の状態を持ってはいけない(Singleton 相当)

    public async Task InvokeAsync(HttpContext ctx, IScopedService svc)
    {   //                                        ↑ Scoped は引数で受け取る
        await _next(ctx);
    }
}

DI の Captive Dependency と同根の問題である。

  • リクエストの都度、IHttpHandlerFactory によって、
    HttpHandlerをインスタンス化される。
  • <httpHandlers> の定義に従って、拡張子や GET,POST などに応じて
    適切なHttpHandlerがインスタンス化される。

補足(IsReusable に注意): IHttpHandler には
IsReusable プロパティがあり、
true にすると HttpModule と同様に使い回される

public class MyHandler : IHttpHandler
{
    public bool IsReusable => false;   // ← 状態を持つなら false
    public void ProcessRequest(HttpContext context) { ... }
}

状態を持たないなら true(インスタンス生成が省ける)、
持つなら false、という判断になる。
迷ったら false にしておく方が安全である。

イベント発生順序

★は初回のみ。

全体の初期処理

  • Global.Application_Start ★
  • IHTTPModule
    • Constructor ★
    • IHTTPModule.Init ★

リクエストの開始処理

  • BeginRequest

    • Global.BeginRequest
    • IHTTPModule.BeginRequest
    • Global.Application_BeginRequest
  • Global.DefaultAuthentication

  • AuthenticateRequest

    • Global.AuthenticateRequest
    • IHTTPModule.AuthenticateRequest
    • Global.Application_AuthenticateRequest
  • PostAuthenticateRequest

    • Global.PostAuthenticateRequest
    • IHTTPModule.PostAuthenticateRequest
  • AuthorizeRequest

    • Global.AuthorizeRequest
    • IHTTPModule.AuthorizeRequest
  • PostAuthorizeRequest

    • Global.PostAuthorizeRequest
    • IHTTPModule.PostAuthorizeRequest
  • ResolveRequestCache

    • Global.ResolveRequestCache
    • IHTTPModule.ResolveRequestCache
  • PostResolveRequestCache

    • Global.PostResolveRequestCache
    • IHTTPModule.PostResolveRequestCache
  • Page.Constructor

  • PostMapRequestHandler

    • Global.PostMapRequestHandler
    • IHTTPModule.PostMapRequestHandler
  • Global.Session_Start ★

  • AcquireRequestState

    • Global.AcquireRequestState
    • IHTTPModule.AcquireRequestState
  • PostAcquireRequestState

    • Global.PostAcquireRequestState
    • IHTTPModule.PostAcquireRequestState
  • PreRequestHandlerExecute

    • Global.PreRequestHandlerExecute
    • IHTTPModule.PreRequestHandlerExecute

ページの開始処理

  • Page.PreInit

  • MasterPage.Constructor

  • Init

    • MasterPage.ContentPlaceHolder
    • MasterPage
    • Page
  • Page

    • InitComplete
    • PreLoad
  • Load

    • Page
    • MasterPage
    • MasterPage.ContentPlaceHolder
  • Page

    • Page.LoadComplete

ページの終了処理

  • PreRender

    • Page
    • MasterPage
    • MasterPage.ContentPlaceHolder
  • Page

    • PreRenderComplete
    • SaveStateComplete
  • Unload

    • MasterPage.ContentPlaceHolder
    • MasterPage
    • Page

補足(ページ内の順序について): この 2 節は
ASP.NET Web Formsのイベント発生順 の内容と対応する。
PageHandlerFactory が生成した Page(= HttpHandler の一種)の
内部で起きていること
が、ここに展開されている。

マスター ページとの順序が逆転している点が実務上の注意点。

Init   : ContentPlaceHolder → MasterPage → Page   (内側から外側へ)
Load   : Page → MasterPage → ContentPlaceHolder   (外側から内側へ)
Unload : ContentPlaceHolder → MasterPage → Page   (内側から外側へ)

Init の時点ではまだ Page の初期化が終わっていないため、
マスター ページ側から Page のプロパティを参照すると
未設定である、という事故が起きる。

リクエストの終了処理

  • PostRequestHandlerExecute

    • Global.PostRequestHandlerExecute
    • IHTTPModule.PostRequestHandlerExecute
  • ReleaseRequestState

    • Global.ReleaseRequestState
    • IHTTPModule.ReleaseRequestState
  • PostReleaseRequestState

    • Global.PostReleaseRequestState
    • IHTTPModule.PostReleaseRequestState
  • UpdateRequestCache

    • Global.UpdateRequestCache
    • IHTTPModule.UpdateRequestCache
  • PostUpdateRequestCache

    • Global.PostUpdateRequestCache
    • IHTTPModule.PostUpdateRequestCache
  • EndRequest

    • Global.EndRequest
    • IHTTPModule.EndRequest
  • PreSendRequestHeaders

    • Global.PreSendRequestHeaders
    • IHTTPModule.PreSendRequestHeaders
  • PreSendRequestContent

    • Global.PreSendRequestContent
    • IHTTPModule.PreSendRequestContent

全体の終了処理

  • Global.Session_End ★
  • IHTTPModule.Dispose ★
  • Global.Application_End ★

補足(Session_End は当てにできない): Session_End
InProc モードのときしか発火しない

セッション モード Session_End
InProc 発火する(ただし保証はされない)
StateServer 発火しない
SQLServer 発火しない
Custom 発火しない

さらに InProc でも、

  • アプリケーション プールのリサイクルでは発火しない
    ASP.NET configperiodicRestart
  • プロセスが落ちれば当然発火しない

「ログアウト時の後処理」を Session_End に書くのは危険であり、
明示的なログアウト処理か、バッチによる期限切れ処理で担保する。

主要なイベントの説明

# イベント 説明
1 BeginRequest リクエストを受信して最初に発生
2 AuthenticateRequest ASP.NET がユーザの認証処理の準備が完了したときに発生
3 PostAuthenticateRequest Post
4 AuthorizeRequest 権限の承認処理の準備が完了したときに発生
5 PostAuthorizeRequest Post
6 ResolveRequestCache リクエストに対してキャッシュからレスポンスを生成するのか
レスポンスを1から生成するのかを決定する
7 PostResolveRequestCache Post
8 PostMapRequestHandler 現在の要求を適切なイベント ハンドラにマップすると発生
9 AcquireRequestState セッション変数の準備処理
10 PostAcquireRequestState Post
11 PreRequestHandlerExecute 各ハンドラ実行直前に発生
12 PostRequestHandlerExecute 各ハンドラ実行直後に発生
13 ReleaseRequestState セッション変数などの値を更新・保存する
14 PostReleaseRequestState Post
15 UpdateRequestCache リクエストキャッシュの更新処理を行う
16 PostUpdateRequestCache Post
17 EndRequest クライアントのブラウザへデータを送信する直前に発生
18 PreSendRequestHeaders HTTP ヘッダーをクライアントに送信する直前に発生
19 PreSendRequestContent コンテンツをクライアントに送信する直前に発生

移行メモ(表記): 原文の「ReleaserequestState」は
ReleaseRequestState の表記揺れと判断し統一した。

補足(どのイベントに書くべきか): 実務でよく使うものを整理しておく。

やりたいこと イベント 理由
URL の書き換え BeginRequest ハンドラーが決まる前
認証(自前) AuthenticateRequest User を差し替えられる
認可(自前) AuthorizeRequest 認証済みの前提で判定
セッションを使う AcquireRequestState 以降 これより前は Session が null
レスポンス本文の加工 BeginRequestResponse.Filter を設定)
ヘッダーの追加・削除 PreSendRequestHeaders 送信直前
ログ出力 EndRequest 状態が確定している

BeginRequestSession を触ったら null だった」というのが
頻出のつまずきで、AcquireRequestState より前にセッションは存在しない

ことが原因である。

リライト

  • リライト ≒ URL の書き換えはHttpHandlerでも可能のようだが、
  • その他の書き換え処理は、要求パイプラインに
    モジュールを「プラグイン」できるHttpModuleを使うことが多い模様。

URL(Request)

URL の書き換えは、

行う。

補足(現在は IIS URL Rewrite が第一候補): 自前で
HttpModule を書くより、IIS の URL Rewrite モジュールを使う方が
簡単で高速である(ネイティブ モジュールで、マネージド コードを通らない)。

<system.webServer>
  <rewrite>
    <rules>
      <rule name="Redirect to HTTPS" stopProcessing="true">
        <match url="(.*)" />
        <conditions><add input="{HTTPS}" pattern="off" /></conditions>
        <action type="Redirect" url="https://{HTTP_HOST}/{R:1}"
                redirectType="Permanent" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>

**Rewrite(内部書き換え)と Redirect(302/301 を返す)**は
別物である点に注意する。

ASP.NET Core では app.UseRewriter()
Microsoft.AspNetCore.Rewrite)が対応する。

app.UseRewriter(new RewriteOptions()
    .AddRedirectToHttps()
    .AddRewrite(@"^old/(.*)", "new/$1", skipRemainingRules: true));

ResponseBody

ResponseBody の書き換えは、HttpModuleで Response.Filter に
デコレートされた ResponseStream を設定することで行う
HttpApplicationHttpModule)。

補足(ASP.NET Core での対応): Response.Filter に相当するのは
HttpResponse.Body の差し替えである。

app.Use(async (ctx, next) =>
{
    var original = ctx.Response.Body;
    using var buffer = new MemoryStream();
    ctx.Response.Body = buffer;

    await next();

    buffer.Seek(0, SeekOrigin.Begin);
    var text = await new StreamReader(buffer).ReadToEndAsync();
    text = text.Replace("foo", "bar");          // 加工

    ctx.Response.Body = original;
    await ctx.Response.WriteAsync(text);
});

ただし、本文を丸ごとバッファリングするため、
メモリ使用量とストリーミング性が犠牲になる

本当に必要か(フロント側や CDN で処理できないか)を
先に検討するのが妥当である。

ResponseHeader

ResponseHeader の書き換えは、
要求パイプライン中の PreSendRequestHeaders ハンドラで行う
HttpApplicationHttpModule)。

補足(不要なヘッダーの削除は web.config でできる): 参考記事が
扱っている「サーバー情報を隠す」目的なら、
コードを書かずに web.config で対処できる

<system.web>
  <httpRuntime enableVersionHeader="false" />   <!-- X-AspNet-Version -->
</system.web>
<system.webServer>
  <httpProtocol>
    <customHeaders>
      <remove name="X-Powered-By" />
      <!-- セキュリティ ヘッダーの追加もここで -->
      <add name="X-Content-Type-Options" value="nosniff" />
      <add name="Strict-Transport-Security" value="max-age=31536000" />
    </customHeaders>
  </httpProtocol>
  <security>
    <requestFiltering removeServerHeader="true" />  <!-- Server(IIS 10+) -->
  </security>
</system.webServer>

ASP.NET Core では ミドルウェアで追加するのが素直である。

app.Use(async (ctx, next) =>
{
    ctx.Response.Headers["X-Content-Type-Options"] = "nosniff";
    await next();
});

PreSendRequestHeaders は ASP.NET Core には無い
(そもそも応答の書き込み前に設定するのが自然な設計になっている)。

参考

microsoft.com

support

Microsoft Learn

違い

HttpModule と HttpHandler

HttpApplication と HttpModule

その他

ライフサイクル

イベント発生順序

リライト


Tags: 移行, .NET開発, ASP.NET

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally