Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Introduction
When proxying a request it's common to modify parts of the request or response to adapt to the destination server's requirements or to flow additional data such as the client's original IP address. This process is implemented via Transforms. Types of transforms are defined globally for the application and then individual routes supply the parameters to enable and configure those transforms. The original request objects are not modified by these transforms, only the proxy requests.
YARP includes a set of built-in request and response transforms that can be used. For more information, see YARP Request and Response Transforms. If those transforms are not sufficient, then custom transforms can be added.
RequestTransform
All request transforms must derive from the abstract base class RequestTransform
. These can freely modify the proxy HttpRequestMessage
. Avoid reading or modifying the request body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on TransformBuilderContext
for discoverability and ease of use.
A request transform may conditionally produce an immediate response such as for error conditions. This prevents any remaining transforms from running and the request from being proxied. This is indicated by setting the HttpResponse.StatusCode
to a value other than 200, or calling HttpResponse.StartAsync()
, or writing to the HttpResponse.Body
or BodyWriter
.
AddRequestTransform is a TransformBuilderContext
extension method that defines a request transform as a Func<RequestTransformContext, ValueTask>
. This allows creating a custom request transform without implementing a RequestTransform
derived class.
ResponseTransform
All response transforms must derive from the abstract base class ResponseTransform. These can freely modify the client HttpResponse
. Avoid reading or modifying the response body as this may disrupt the proxying flow. Consider also adding a parametrized extension method on TransformBuilderContext
for discoverability and easy of use.
AddResponseTransform is a TransformBuilderContext
extension method that defines a response transform as a Func<ResponseTransformContext, ValueTask>
. This allows creating a custom response transform without implementing a ResponseTransform
derived class.
ResponseTrailersTransform
All response trailers transforms must derive from the abstract base class ResponseTrailersTransform. These can freely modify the client HttpResponse trailers. These run after the response body and should not attempt to modify the response headers or body. Consider also adding a parametrized extension method on TransformBuilderContext
for discoverability and easy of use.
AddResponseTrailersTransform is a TransformBuilderContext
extension method that defines a response trailers transform as a Func<ResponseTrailersTransformContext, ValueTask>
. This allows creating a custom response trailers transform without implementing a ResponseTrailersTransform
derived class.
Request body transforms
YARP does not provide any built in transforms for modifying the request body. However, the body can be modified by custom transforms.
Be careful about which kinds of requests are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like Content-Length
.
The below example uses simple, inefficient buffering to transform requests. A more efficient implementation would wrap and replace HttpContext.Request.Body
with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance.
This sample requires YARP 1.1, see 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;
}
});
});
Custom transforms can only modify a request body if one is already present. They can't add a new body to a request that doesn't have one (for example, a POST request without a body or a GET request). If you need to add a body for a specific HTTP method and route, you must do so in middleware that runs before YARP, not in a transform.
The following middleware demonstrates how to add a body to a request that doesn't have one:
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;
}
}
Note
You can use context.GetRouteModel().Config.RouteId
in middleware to conditionally apply this logic for specific YARP routes.
Response body transforms
YARP does not provide any built in transforms for modifying the response body. However, the body can be modified by custom transforms.
Be careful about which kinds of responses are modified, how much data gets buffered, enforcing timeouts, parsing untrusted input, and updating the body-related headers like Content-Length
. You may need to decompress content before modifying it, as indicated by the Content-Encoding header, and afterwards re-compress it or remove the header.
The below example uses simple, inefficient buffering to transform responses. A more efficient implementation would wrap the stream returned by ReadAsStreamAsync()
with a stream that performed the needed modifications as data was proxied from client to server. That would also require removing the Content-Length header since the final length would not be known in advance.
.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 provides the functionality of AddTransforms
described above as well as DI integration and validation support.
ITransformProvider
's can be registered in DI by calling AddTransforms. Multiple ITransformProvider
implementations can be registered and all will be run.
ITransformProvider
has two methods, Validate
and Apply
. Validate
gives you the opportunity to inspect the route for any parameters that are needed to configure a transform, such as custom metadata, and to return validation errors on the context if any needed values are missing or invalid. The Apply
method provides the same functionality as AddTransform as discussed above, adding and configuring transforms per route.
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
Developers that want to integrate their custom transforms with the Transforms
section of configuration can implement an ITransformFactory. This should be registered in DI using the AddTransformFactory<T>()
method. Multiple factories can be registered and all will be used.
ITransformFactory
provides two methods, Validate
and Build
. These process one set of transform values at a time, represented by a IReadOnlyDictionary<string, string>
.
The Validate
method is called when loading a configuration to verify the contents and report all errors. Any reported errors will prevent the configuration from being applied.
The Build
method takes the given configuration and produces the associated transform instances for the route.
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
and Build
return true
if they've identified the given transform configuration as one that they own. A ITransformFactory
may implement multiple transforms. Any RouteConfig.Transforms
entries not handled by any ITransformFactory
will be considered configuration errors and prevent the configuration from being applied.
Consider also adding parametrized extension methods on RouteConfig
like WithTransformQueryValue
to facilitate programmatic route construction.
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;
});
}
ASP.NET Core