Sisk

リクエストハンドリング

このページは英語から自動翻訳されています。 原文を読む

リクエストハンドラ(ミドルウェアとも呼ばれます)は、ルーター上でリクエストが実行される前後に実行される関数です。ルート単位またはルーター単位で定義できます。

リクエストハンドラには 2 種類あります。

  • BeforeResponse: ルーターアクションを呼び出す前にリクエストハンドラが実行されることを示します。
  • AfterResponse: ルーターアクションを呼び出した後にリクエストハンドラが実行されることを示します。このコンテキストで HTTP レスポンスを送信すると、ルーターのアクションレスポンスが上書きされます。

両方のリクエストハンドラは、実際のルーターコールバック関数のレスポンスを上書きできます。なお、リクエストハンドラは認証やコンテンツの検証、情報の保存、ログ記録、レスポンス前後に実行できるその他の処理など、リクエストの検証に便利です。

このように、リクエストハンドラは実行を途中で中断し、サイクルが完了する前にレスポンスを返すことで、以降の処理をすべて破棄できます。

例: ユーザー認証リクエストハンドラが認証に失敗したとします。この場合、リクエストライフサイクルは継続できず、処理が停止します。もしこのハンドラが 2 番目の位置にある場合、3 番目以降は評価されません。

リクエストハンドラの作成 #

リクエストハンドラを作成するには、IRequestHandler インターフェイスを継承したクラスを次のように作ります。

Middleware/AuthenticateUserRequestHandler.csC#
public class AuthenticateUserRequestHandler : IRequestHandler
{
    public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse;

    public HttpResponse? Execute(HttpRequest request, HttpContext context)
    {
        if (request.Headers.Authorization != null)
        {
            // null を返すと、リクエストサイクルを継続できることを示します
            return null;
        }
        else
        {
            // HttpResponse オブジェクトを返すと、このレスポンスが隣接するレスポンスを上書きすることを示します
            return new HttpResponse(System.Net.HttpStatusCode.Unauthorized);
        }
    }
}

上の例では、リクエストに Authorization ヘッダーが存在すれば処理を続行し、次のリクエストハンドラまたはルーターコールバックが呼び出されます。ExecutionMode プロパティで AfterResponse として実行され、かつ非 null の値を返す場合は、ルーターのレスポンスが上書きされます。

リクエストハンドラが null を返すと、リクエストは継続され、次のオブジェクトが呼び出されるか、ルーターのレスポンスでサイクルが終了します。

警告

リクエストハンドラのインスタンスは一度作成され、処理するすべてのリクエスト(同時実行リクエストを含む)で共有されます。リクエストハンドラのクラス自体に個別のリクエスト状態(現在のユーザー、解析済みトークン、タイマーなど)をフィールドやプロパティとして保持しないでください。その値は他のリクエストに漏れます。代わりに HttpContext の RequestBag にリクエスト単位の値を格納してください。

C#
public HttpResponse? Execute(HttpRequest request, HttpContext context)
{
    // 誤り: this.currentUser = FindUser(request);
    context.RequestBag.Set(FindUser(request));
    return null;
}

組み込みの RequestHandler クラスを継承する場合は、Next() を返すことで意図を明示できます。

C#
public class AuthenticateUserRequestHandler : RequestHandler
{
    public override HttpResponse? Execute(HttpRequest request, HttpContext context)
    {
        if (request.Headers.Authorization is not null)
            return Next();

        return new HttpResponse(System.Net.HttpStatusCode.Unauthorized);
    }
}

I/O が必要なハンドラは、AsyncRequestHandler を継承します。

C#
public class LoadUserRequestHandler : AsyncRequestHandler
{
    public override async Task<HttpResponse?> ExecuteAsync(HttpRequest request, HttpContext context)
    {
        var user = await UserRepository.FindAsync(request.Headers.Authorization, request.DisconnectToken);
        if (user is null)
            return new HttpResponse(System.Net.HttpStatusCode.Unauthorized);

        request.Bag.Set(user);
        return Next();
    }
}

小規模なインラインハンドラは RequestHandler.Create または AsyncRequestHandler.Create でも作成できます。

C#
var requireJson = RequestHandler.Create((request, context) =>
{
    if (request.Headers.ContentType?.Contains("application/json") == true)
        return null;

    return new HttpResponse(System.Net.HttpStatusCode.UnsupportedMediaType);
});

単一ルートにリクエストハンドラを関連付ける #

ルートに対して 1 つ以上のリクエストハンドラを定義できます。

Router.csC#
mainRouter.Map(RouteMethod.Get, "/", IndexPage, new IRequestHandler[]
{
    new AuthenticateUserRequestHandler(),     // before request handler
    new ValidateJsonContentRequestHandler(),  // before request handler
    //                                        -- method IndexPage will be executed here
    new WriteToLogRequestHandler()            // after request handler
});

または Route オブジェクトを作成して設定します。

Router.csC#
Route indexRoute = Route.Get("/", IndexPage);
indexRoute.RequestHandlers = new IRequestHandler[]
{
    new AuthenticateUserRequestHandler()
};
mainRouter.Map(indexRoute);

ルーターにリクエストハンドラを関連付ける #

すべてのルートで実行されるグローバルリクエストハンドラを定義できます。

Router.csC#
mainRouter.GlobalRequestHandlers = new IRequestHandler[]
{
    new AuthenticateUserRequestHandler()
};

属性にリクエストハンドラを関連付ける #

メソッド属性とルート属性に併せて、リクエストハンドラを属性として定義できます。

Controller/MyController.csC#
public class MyController
{
    [RouteGet("/")]
    [RequestHandler<AuthenticateUserRequestHandler>]
    static HttpResponse Index(HttpRequest request)
    {
        return new HttpResponse() {
            Content = new StringContent("Hello world!")
        };
    }
}

ここではインスタンスではなくハンドラの型を指定する必要があります。これにより、ルーターのパーサーがハンドラをインスタンス化します。コンストラクタ引数は ConstructorArguments プロパティで渡せます。

例:

Controller/MyController.csC#
[RequestHandler<AuthenticateUserRequestHandler>("arg1", 123, ...)]
public HttpResponse Index(HttpRequest request)
{
    return res = new HttpResponse() {
        Content = new StringContent("Hello world!")
    };
}

独自の属性を作成して RequestHandler を実装することも可能です。

Middleware/Attributes/AuthenticateAttribute.csC#
public class AuthenticateAttribute : RequestHandlerAttribute
{
    public AuthenticateAttribute() : base(typeof(AuthenticateUserRequestHandler), ConstructorArguments = new object?[] { "arg1", 123, ... })
    {
        ;
    }
}

使用例:

Controller/MyController.csC#
[Authenticate]
static HttpResponse Index(HttpRequest request)
{
    return res = new HttpResponse() {
        Content = new StringContent("Hello world!")
    };
}

グローバルリクエストハンドラをバイパスする #

ルートにグローバルリクエストハンドラを設定した後、特定のルートでそのハンドラを無視できます。

Router.csC#
var myRequestHandler = new AuthenticateUserRequestHandler();
mainRouter.GlobalRequestHandlers = new IRequestHandler[]
{
    myRequestHandler
};

Route publicRoute = Route.Get("/", IndexPage);
publicRoute.Name = "My route";
publicRoute.BypassGlobalRequestHandlers = new IRequestHandler[]
{
    myRequestHandler,                    // ok: the same instance of what is in the global request handlers
    new AuthenticateUserRequestHandler() // wrong: will not skip the global request handler
};

mainRouter.Map(publicRoute);

注記

リクエストハンドラをバイパスする場合、スキップしたいハンドラの 同じインスタンス を使用しなければなりません。別のインスタンスを作成すると参照が変わり、グローバルリクエストハンドラはスキップされません。GlobalRequestHandlers と BypassGlobalRequestHandlers の両方で同一のハンドラ参照を使用することを忘れないでください。

ドキュメントと API リファレンスを検索するには入力してください。