WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

サーバー側で応答コンテンツの取得 (CORE)

by WebSurfer 2022年7月7日 16:30

ASP.NET Core Web アプリの応答ボディの文字列を、サーバー側でのログなどの目的で、取得する方法を書きます。先の記事「サーバー側で応答コンテンツの取得」の ASP.NET Core 版です。

応答ボディの文字列

.NET Framework 版 ASP.NET で利用した HttpModule、HttpResponse.Filter プロパティ、HttpResponse.OutputStream プロパティは ASP.NET Core ではサポートされていません。

代わりにカスタム MiddlewareHttpResponse.Body プロパティを使って同様なことを行います。

ただし、HttpResponse.Body プロパティで取得できる Stream は CanRead, CanSeek が false になっており読むことができないという問題があります。

その解決に、先の記事「HttpRequest.Body から読み取る方法 (CORE)」で書いた EnableBuffering メソッドが使えるかと思ったのですが、応答側 HttpResponse ではサポートされていませんでした。

やむを得ず、Middleware で next.Invoke の前に応答ボディ用の Stream を MemoryStream に差し替えて、next.Invoke の後で MemoryStream に書き込まれた応答ボディを取得するという手段を取りました。

そのサンプルコードは以下の通りです。デバッグ実行すると上の画像ように「出力」ウィンドウに応答ボディの文字列が取得できます。

namespace MvcCore6App3.Middleware
{
    public class ResponseContentLogMiddleware
    {
        private readonly RequestDelegate _next;

        public ResponseContentLogMiddleware(RequestDelegate next)
        {
            this._next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            // この例では Home/Privacy のみログを取るという前提
            if (context.Request.Path.ToString().Contains("/Home/Privacy"))
            {
                // 応答ボディの Stream を取得して保持
                Stream responseStream = context.Response.Body;

                // 応答ボディの文字列を取得する
                string bodyContent = "";

                try
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        // 応答ストリームを MemoryStream に差し替えて、それに
                        // 応答ボディを取得。_next.Invoke の前でないとダメ
                        context.Response.Body = memoryStream;

                        await _next.Invoke(context);

                        // この時点ではすでに MemoryStream には応答ボディは
                        // 書き込まれている

                        if (memoryStream.Length > 0L &&
                            memoryStream.CanRead &&
                            memoryStream.CanSeek)
                        {
                            memoryStream.Position = 0L;
                            var reader = new StreamReader(memoryStream);
                            bodyContent = await reader.ReadToEndAsync();

                            // 確認用(デバッグ実行で Visual Studio の「出力」
                            // ウィンドウに表示される)
                            System.Diagnostics.Debug.Write(bodyContent);

                            memoryStream.Position = 0L;

                            // MemoryStream に取得した応答ボディのバイト列を
                            // responseStream にコピー
                            await memoryStream.CopyToAsync(responseStream);
                        }
                    }
                }
                finally
                {
                    // 上のコードで MemoryStream に差し替えた応答ボディの
                    // Stream を元に戻す
                    context.Response.Body = responseStream;
                }
            }
            else
            {
                // 何もしない場合でも以下のコードは必須。これが無いとミドル
                // ウェアのチェーンの途中で止まってしまう
                await _next.Invoke(context);
            }
        }
    }
}

上の Middleware が動くようにするには Program.cs (.NET 5.0 以前では Startup.cs) での設定が必要です。以下のコードで「追記」とコメントした一行を追加します。

// ・・・前略・・・

app.UseAuthentication();
app.UseAuthorization();

// 追記
app.UseMiddleware<ResponseContentLogMiddleware>();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

HttpResponse.Body プロパティで取得できる Stream が読めないのは、HttpRequest.Body と同様に、"lightweight and performant as possible" ということを狙ってのことではないかと思います。

なので、上記のようなことをすると性能上の劣化が生じるかもしれません。どうしてもログが必要と言うような場合にとどめておいた方が良さそうな気がします。

Tags: , , ,

CORE

サーバー側で応答コンテンツの取得

by WebSurfer 2018年7月14日 13:26

ASP.NET Web アプリの応答コンテンツの文字列を、サーバー側でのログ取得などの目的で取得する方法を書きます。(あまり需要はないかもしれませんが)

参考にしたのは stackoverflow のスレッド Logging raw HTTP request/response in ASP.NET MVC & IIS7 です。その記事を見れば情報としては十分かもしれませんが、リンク切れになると困るし、記事にはない HTTP モジュールを使っての設定方法も追加して書いておきます。

基本的には、応答コンテンツは応答ストリームに含まれますのでそれを何らかの手段で取得するということになります。

stackoverflow の記事では、応答ストリームをラッピングするフィルターを作成し、フィルターの中に応答ストリームとは別に MemoryStream を用意し、ASP.NET が応答ストリームに書き込む際 MemoryStream にも同じ内容を書き込むようにし(要するに MemoryStream にコピーを作成し)、MemoryStream から応答コンテンツを取得するという方法が紹介されています。

下のコード例を見てください。その中の OutputFilterStream クラスがラッピングフィルターです。stackoverflow の記事のコードをそのままコピーしたものです。

HTTP モジュール(下のコードでは ResponseContentLogHttpModule クラス)を用い、BeginRequest イベントのタイミングでラッピングフィルターを HttpResponse.Filter プロパティに設定します。(注:EndRequest のタイミングではダメです。ストリームにはその前に書き込まれるので)

HttpResponse.Filter プロパティの NSDN ライブラリの説明には "伝送する前に HTTP エンティティ本体を変更するために使用される、ラッピングフィルターオブジェクトを取得または設定します" とありますが、OutputFilterStream はコンテンツの変更は一切せずそのまま応答として返し、MemoryStream にコピーを取得することのみ行います。

BeginRequest のタイミングで設定したラッピングフィルターオブジェクトを EndRequest のタイミングで取得し MemoryStream にコピーされた応答コンテンツを取得します。

BeginRequest のタイミ���グから EndRequest のタイミングまでラッピングフィルターオブジェクトを保持するには HttpContext.Items プロパティを利用します。下のコード例を見てください。

クラスのフィールド等で保持するのは NG という報告がありましたので注意してください。HttpContext.Items を使えば、少なくとも HttpContext が存在する限りラッピングフィルターオブジェクトは保持されるが(GC の対象にはならないが)、クラスのフィールド変数ではその限りではないということのようです。

以下に HTTP モジュールとラッピングフィルターのコード例をアップしておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;

// HTTP モジュール
public class ResponseContentLogHttpModule : IHttpModule
{
  public ResponseContentLogHttpModule()
  {
  }

  public String ModuleName
  {
    get { return "ResponseContentLogHttpModule"; }
  }

  public void Init(HttpApplication application)
  {
    application.BeginRequest += this.BeginRequest;
    application.EndRequest += this.EndRequest;
  }

  private void BeginRequest(Object source, EventArgs e)
  {
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    string filePath = context.Request.FilePath;
    string fileExtension =
        VirtualPathUtility.GetExtension(filePath);

    // とりあえず .aspx のみ対象(.js, .css 等は対象外)
    if (fileExtension.Equals(".aspx"))
    {
      HttpResponse response = context.Response;
      var filter = new OutputFilterStream(response.Filter);
      response.Filter = filter;

      // OutputFilterStream オブジェクトを EndRequest で
      // 利用できるよう HttoContext.Items に参照を保持。
      // (注:クラスのフィールド等で保持するのは NG)
      context.Items["FilterStream"] = filter;
    }
  }

  private void EndRequest(Object source, EventArgs e)
  {
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    string filePath = context.Request.FilePath;
    string fileExtension =
        VirtualPathUtility.GetExtension(filePath);

    // とりあえず .aspx のみ対象(.js, .css 等は対象外)
    if (fileExtension.Equals(".aspx"))
    {
      // BegineRequest で HttpContext.Items に保持した
      // OutputFilterStream オブジェクトへの参照を取得
      var filter =
        (OutputFilterStream)context.Items["FilterStream"];

      // 応答コンテンツの文字列を取得
      string responseContent = filter.ReadStream();
    }
  }

  // IHttpModule に定義されているので空でも以下が必要
  public void Dispose() { }
}

// ラッピングフィルタークラス
// stackoverflow の記事のコードをそのままコピー
public class OutputFilterStream : Stream
{
  private readonly Stream InnerStream;
  private readonly MemoryStream CopyStream;

  public OutputFilterStream(Stream inner)
  {
    this.InnerStream = inner;
    this.CopyStream = new MemoryStream();
  }

  public string ReadStream()
  {
    lock (this.InnerStream)
    {
      if (this.CopyStream.Length <= 0L ||
          !this.CopyStream.CanRead ||
          !this.CopyStream.CanSeek)
      {
        return String.Empty;
      }

      long pos = this.CopyStream.Position;
      this.CopyStream.Position = 0L;
      try
      {
        return new StreamReader(this.CopyStream).ReadToEnd();
      }
      finally
      {
        try
        {
          this.CopyStream.Position = pos;
        }
        catch { }
      }
    }
  }

  public override bool CanRead
  {
    get { return this.InnerStream.CanRead; }
  }

  public override bool CanSeek
  {
    get { return this.InnerStream.CanSeek; }
  }

  public override bool CanWrite
  {
    get { return this.InnerStream.CanWrite; }
  }

  public override void Flush()
  {
    this.InnerStream.Flush();
  }

  public override long Length
  {
    get { return this.InnerStream.Length; }
  }

  public override long Position
  {
    get { return this.InnerStream.Position; }
    set
    {
      this.CopyStream.Position =
      this.InnerStream.Position = value;
    }
  }

  public override int Read(byte[] buffer,
                          int offset, int count)
  {
    return this.InnerStream.Read(buffer, offset, count);
  }

  public override long Seek(long offset, SeekOrigin origin)
  {
    this.CopyStream.Seek(offset, origin);
    return this.InnerStream.Seek(offset, origin);
  }

  public override void SetLength(long value)
  {
    this.CopyStream.SetLength(value);
    this.InnerStream.SetLength(value);
  }

  public override void Write(byte[] buffer,
                             int offset, int count)
  {
    this.CopyStream.Write(buffer, offset, count);
    this.InnerStream.Write(buffer, offset, count);
  }
}

上の HTTP モジュールが動く ようにするには web.config での設定が必要ですので注意してください。以下に設定例を書いておきます。

<system.webServer>
  <modules>
    <add name="ResponseContentLogHttpModule" 
         type="ResponseContentLogHttpModule"/>
  </modules>
</system.webServer>

Tags: , ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar