次の方法で共有


要求応答変換機能の拡張性

イントロダクション

要求をプロキシする場合、要求または応答の一部を変更して、移行先サーバーの要件に合わせたり、クライアントの元の IP アドレスなどの追加データをフローしたりするのが一般的です。 このプロセスは Transforms を使用して実装されます。 変換の種類はアプリケーションに対してグローバルに定義され、個々のルートは、それらの変換を有効にして構成するためのパラメーターを提供します。 元の要求オブジェクトはこれらの変換によって変更されず、プロキシ要求のみが変更されます。

YARP には、使用できる一連の組み込みの要求および応答変換が含まれています。 詳細については、「 YARP 要求変換と応答変換」を参照してください。 これらの変換が十分でない場合は、カスタム変換を追加できます。

RequestTransform

すべての要求変換は、抽象基底クラスの RequestTransformから派生する必要があります。 これにより、プロキシ HttpRequestMessageを自由に変更できます。 プロキシ フローが中断される可能性があるため、要求本文の読み取りや変更は避けてください。 また、検出可能性と使いやすさのために、 TransformBuilderContext にパラメーター化された拡張メソッドを追加することも検討してください。

要求変換では、エラー条件などの即時応答が条件付きで生成される場合があります。 これにより、残りの変換が実行されず、要求がプロキシされるのを防ぐことができます。 これは、 HttpResponse.StatusCode を 200 以外の値に設定するか、 HttpResponse.StartAsync()を呼び出すか、 HttpResponse.Body または BodyWriterに書き込むことで示されます。

AddRequestTransformは、要求変換をTransformBuilderContextとして定義するFunc<RequestTransformContext, ValueTask>拡張メソッドです。 これにより、 RequestTransform 派生クラスを実装せずにカスタム要求変換を作成できます。

ResponseTransform

すべての応答変換は、抽象基底クラスの ResponseTransformから派生する必要があります。 これらは、クライアント HttpResponseを自由に変更できます。 プロキシ フローが中断される可能性があるため、応答本文の読み取りや変更は避けてください。 また、検出可能性と使いやすさのために、 TransformBuilderContext にパラメーター化された拡張メソッドを追加することも検討してください。

AddResponseTransformは、応答変換をTransformBuilderContextとして定義するFunc<ResponseTransformContext, ValueTask>拡張メソッドです。 これにより、 ResponseTransform 派生クラスを実装せずにカスタム応答変換を作成できます。

ResponseTrailersTransform

すべての応答トレーラー変換は、抽象基底クラスの ResponseTrailersTransformから派生する必要があります。 これらは、クライアント HttpResponse トレーラーを自由に変更できます。 これらは応答本文の後に実行され、応答ヘッダーまたは本文の変更を試みるべきではありません。 また、検出可能性と使いやすさのために、 TransformBuilderContext にパラメーター化された拡張メソッドを追加することも検討してください。

AddResponseTrailersTransformは、応答トレーラー変換をTransformBuilderContextとして定義するFunc<ResponseTrailersTransformContext, ValueTask>拡張メソッドです。 これにより、 ResponseTrailersTransform 派生クラスを実装せずにカスタム応答トレーラー変換を作成できます。

リクエストボディの変換

YARP では、要求本文を変更するための組み込みの変換は提供されません。 ただし、本文はカスタム変換によって変更できます。

変更される要求の種類、バッファーに格納されるデータの量、タイムアウトの強制、信頼されていない入力の解析、 Content-Lengthなどの本文関連ヘッダーの更新に注意してください。

次の例では、単純で非効率的なバッファリングを使用して要求を変換します。 より効率的な実装では、 HttpContext.Request.Body をラップし、データがクライアントからサーバーにプロキシされるため、必要な変更を実行したストリームに置き換えます。 また、最終的な長さが事前に不明になるため、Content-Length ヘッダーを削除する必要もあります。

このサンプルには YARP 1.1 が必要です。 https://github.com/microsoft/reverse-proxy/pull/1569を参照してください。

.AddTransforms(context =>
{
    context.AddRequestTransform(async requestContext =>
    {
        using var reader =
            new StreamReader(requestContext.HttpContext.Request.Body);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();
        if (!string.IsNullOrEmpty(body))
        {
            body = body.Replace("Alpha", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            requestContext.HttpContext.Request.Body = new MemoryStream(bytes);
            // Request headers are copied before transforms are invoked, update any
            // needed headers on the ProxyRequest
            requestContext.ProxyRequest.Content.Headers.ContentLength =
                bytes.Length;
        }
    });
});

カスタム変換は、要求本文が既に存在する場合にのみ変更できます。 本文がない要求(例えば、本文のない POST 要求や GET 要求)に新しい本文を追加することはできません。 特定の HTTP メソッドとルートの本文を追加する必要がある場合は、変換ではなく YARP より前に実行される ミドルウェア で追加する必要があります。

次のミドルウェアは、本文がない要求に本文を追加する方法を示しています。

public class AddRequestBodyMiddleware
{
    private readonly RequestDelegate _next;

    public AddRequestBodyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Only modify specific route and method
        if (context.Request.Method == HttpMethods.Get &&
            context.Request.Path == "/special-route")
        {
            var bodyContent = "key=value";
            var bodyBytes = Encoding.UTF8.GetBytes(bodyContent);

            // Create a new request body
            context.Request.Body = new MemoryStream(bodyBytes);
            context.Request.ContentLength = bodyBytes.Length;

            // Replace IHttpRequestBodyDetectionFeature so YARP knows
            // a body is present
            context.Features.Set<IHttpRequestBodyDetectionFeature>(
                new CustomBodyDetectionFeature());
        }

        await _next(context);
    }

    // Helper class to indicate the request can have a body
    private class CustomBodyDetectionFeature : IHttpRequestBodyDetectionFeature
    {
        public bool CanHaveBody => true;
    }
}

ミドルウェアで context.GetRouteModel().Config.RouteId を使用して、特定の YARP ルートにこのロジックを条件付きで適用できます。

応答本文の変換

YARP では、応答本文を変更するための組み込みの変換は提供されません。 ただし、本文はカスタム変換によって変更できます。

変更される応答の種類、バッファーに格納されるデータの量、タイムアウトの強制、信頼されていない入力の解析、 Content-Lengthなどの本文関連ヘッダーの更新に注意してください。 Content-Encoding ヘッダーで示されているように、コンテンツを変更する前にコンテンツを展開する必要がある場合があります。その後で再圧縮するか、ヘッダーを削除することがあります。

次の例では、単純で非効率的なバッファリングを使用して応答を変換します。 より効率的な実装では、 ReadAsStreamAsync() によって返されるストリームを、クライアントからサーバーにデータがプロキシされた場合に必要な変更を実行したストリームでラップされます。 また、最終的な長さが事前に不明になるため、Content-Length ヘッダーを削除する必要もあります。

.AddTransforms(context =>
{
    context.AddResponseTransform(async responseContext =>
    {
        var stream =
            await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();

        if (!string.IsNullOrEmpty(body))
        {
            responseContext.SuppressResponseBody = true;

            body = body.Replace("Bravo", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            responseContext.HttpContext.Response.ContentLength = bytes.Length;
            // Response headers are copied before transforms are invoked, update
            // any needed headers on the HttpContext.Response
            await responseContext.HttpContext.Response.Body.WriteAsync(bytes);
        }
    });
});

ITransformProvider

ITransformProvider は、上記の AddTransforms の機能と、DI 統合と検証のサポートを提供します。

ITransformProvider's は、 AddTransformsを呼び出すことによって DI に登録できます。 複数の ITransformProvider 実装を登録でき、すべてが実行されます。

ITransformProvider には、 ValidateApplyの 2 つのメソッドがあります。 Validate では、カスタム メタデータなどの変換を構成するために必要なパラメーターをルートで検査し、必要な値が見つからないか無効な場合にコンテキストで検証エラーを返す機会を提供します。 Applyメソッドは、前述のように AddTransform と同じ機能を提供し、ルートごとに変換を追加および構成します。

services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransforms<MyTransformProvider>();
internal class MyTransformProvider : ITransformProvider
{
    public void ValidateRoute(TransformRouteValidationContext context)
    {
        // Check all routes for a custom property and validate the associated
        // transform data
        if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ??
            false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }

    public void ValidateCluster(TransformClusterValidationContext context)
    {
        // Check all clusters for a custom property and validate the associated
        // transform data.
        if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value)
            ?? false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }

    public void Apply(TransformBuilderContext transformBuildContext)
    {
        // Check all routes for a custom property and add the associated transform.
        if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata",
            out var value) ?? false)
            || (transformBuildContext.Cluster?.Metadata?.TryGetValue(
            "CustomMetadata", out value) ?? false))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomMetadata value is required");
            }

            transformBuildContext.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomMetadata"), value);

                return default;
            });
        }
    }
}

ITransformFactory

カスタム変換を構成の Transforms セクションと統合する開発者は、 ITransformFactoryを実装できます。 これは、 AddTransformFactory<T>() メソッドを使用して DI に登録する必要があります。 複数の工場を登録でき、すべてが使用されます。

ITransformFactory には、 ValidateBuildの 2 つのメソッドが用意されています。 これらは、 IReadOnlyDictionary<string, string>で表される一度に 1 セットの変換値を処理します。

Validate メソッドは、構成を読み込んで内容を確認し、すべてのエラーを報告するときに呼び出されます。 報告されたエラーにより、構成が適用されなくなります。

Build メソッドは、指定された構成を受け取り、ルートに関連付けられた変換インスタンスを生成します。

services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransformFactory<MyTransformFactory>();
internal class MyTransformFactory : ITransformFactory
{
    public bool Validate(TransformRouteValidationContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomTransform value is required"));
            }

            return true; // Matched
        }

        return false;
    }

    public bool Build(TransformBuilderContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomTransform value is required");
            }

            context.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomTransform"), value);
                return default;
            });

            return true;
        }

        return false;
    }
}

Validate および Build は、指定された変換構成を自分たちのものとして識別した場合、true を返します。 ITransformFactoryは、複数の変換を実装できます。 RouteConfig.Transformsによって処理されないITransformFactoryエントリは、構成エラーと見なされ、構成が適用されなくなります。

また、プログラムによるルート構築を容易にするために、RouteConfigなどのWithTransformQueryValueにパラメーター化された拡張メソッドを追加することも検討してください。

public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig,
    string queryKey, string value, bool append = true)
{
    var type = append ? QueryTransformFactory.AppendKey :
        QueryTransformFactory.SetKey;
    return routeConfig.WithTransform(transform =>
    {
        transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;
        transform[type] = value;
    });
}