
YARP (Yet Another Reverse Proxy) is a comprehensive reverse proxy library developed by Microsoft for .NET that continues to see a strong level of adoption.
In my previous post, I explored getting started with YARP, walking through how to set up and run an ASP.NET Core web application that uses YARP to intercept and route requests to a configured cluster.
In this post, I’m going to provide some code samples that demonstrate how to transform requests and responses with YARP via Transform Providers.
Setup
Please note the assumed setup of the environment below before proceeding.
If you followed all the steps from my previous Getting started with YARP post, you’ll have everything in place that is needed, apart from a minor change to the configuration. For reference, the contents of the appsettings.json file from the sample project, plus the new piece of configuration I’ve now added to support this post, are as follows.
{ Â Â "Logging":Â { Â Â Â Â "LogLevel":Â { Â Â Â Â Â Â "Default":Â "Information", Â Â Â Â Â Â "Microsoft.AspNetCore":Â "Warning" Â Â Â Â } Â Â }, Â Â "AllowedHosts":Â "*", Â Â "ReverseProxy":Â { Â Â Â Â "Routes":Â { Â Â Â Â Â Â "todos":Â { Â Â Â Â Â Â Â Â "ClusterId":Â "jsonplaceholder", Â Â Â Â Â Â Â Â "Match":Â { Â Â Â Â Â Â Â Â Â Â "Path":Â "/todos/{**catch-all}" Â Â Â Â Â Â Â Â }, Â Â Â Â Â Â Â Â "Metadata":Â { Â Â Â Â Â Â Â Â Â Â "TenantId":Â "Acme" Â Â Â Â Â Â Â Â } Â Â Â Â Â Â } Â Â Â Â }, Â Â Â Â "Clusters":Â { Â Â Â Â Â Â "jsonplaceholder":Â { Â Â Â Â Â Â Â Â "Destinations":Â { Â Â Â Â Â Â Â Â Â Â "default":Â { Â Â Â Â Â Â Â Â Â Â Â Â "Address":Â "https://jsonplaceholder.typicode.com/" Â Â Â Â Â Â Â Â Â Â } Â Â Â Â Â Â Â Â } Â Â Â Â Â Â } Â Â Â Â } Â Â } }
The above configuration will route any requests to /todos to the JSONPlaceholder server.
The additional bit I’ve added is the following within the “todos” route section.
"Metadata":Â { "TenantId":Â "Acme" }
The configured “Metadata” properties can be accessed within YARP transformation logic to make decisions on whether specific transformation logic should be applied and to control any other aspect of how requests and responses are handled.
With this updated configuration in place, we can continue to the following section to explore how requests and responses can be transformed.
Transform Providers
There are a few different ways to transform requests and responses using YARP.
You can configure routes using predefined configuration transforms for simple scenarios, or implement the RequestTransform or ResponseTransform abstract classes to perform complex logic, or you can register transform logic inline when registering the YARP reverse proxy services in your Program.cs file.
Personally, my preference is to create one or more Transform Provider classes that implement the ITransformProvider interface, as I believe it provides the most powerful solution with the most options and facilitates dependency injection of required services where needed, so this will be the focus of the post in the sections that follow.
Response Transforms
To transform a response, we can create a class that implements the ITransformProvider interface and add a response transform to the TransformBuilderContext context within the Apply method.
Here’s a fun example where we spoof the ‘Server’ header that is returned to the client.
using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace YarpDemo.Transforms; public class ServerResponseTransformProvider : ITransformProvider {     public void ValidateRoute(TransformRouteValidationContext context)     {         // No custom validation required.     }     public void ValidateCluster(TransformClusterValidationContext context)     {         // No custom validation required.     }     public void Apply(TransformBuilderContext context)     {         context.AddResponseTransform(TransformResponse);     }     private static ValueTask TransformResponse(ResponseTransformContext context)     {         if (context.ProxyResponse?.Headers?.Server             .FirstOrDefault()?             .ToString()             .Equals("cloudflare", StringComparison.OrdinalIgnoreCase) is true)         {             context.HttpContext.Response.Headers.Server = new StringValues("apache");         }         return ValueTask.CompletedTask;     } }
Note that you’ll need to adjust the namespace to suit your project.
The ITransformProvider interface requires the following three methods.
ValidateRouteValidateClusterApply
In the above code, there is no implementation specified for the ValidateRoute and ValidateCluster methods. However, if you were to provide an implementation, you could check the router/cluster configuration and record an error as follows.
context.Errors.Add(new InvalidOperationException("Server name not configured."));
The Apply method is where the response transform function needs to be registered. The AddResponseTransform method is called on the context to achieve this.
The private TransformResponse method is where the transformation logic is defined.
In this example, the code inspects the HTTP headers on the proxy response, checking the standard ‘Server’ header value to see if it equals ‘cloudflare’ (this should be the case if using the appsettings.json configuration listed in the Setup section, since Cloudflare sits in front of JSONPlaceholder). If so, we’ll set the value of the ‘Server’ header on the response that is returned to the client to ‘apache’ instead.
Of course, this isn’t something that you would usually do for a practical application. However, adding new headers and/or manipulating the response in some other way is a common requirement. There are many other properties on the proxy response object that you can inspect, and you are free to manipulate any aspect of the response that is returned to the client. For example, you could change the HTTP Status Code that is returned depending on some condition.
Before running the application to test things out, you’ll need to register the Transform Provider in your Program.cs file as follows.
builder.Services.AddReverseProxy() Â Â Â Â .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) Â Â Â .AddTransforms<ServerResponseTransformProvider>();
You can use the browser Developer Tools to inspect the Network request that is issued when navigating to /todos and verify that the Server header in the response has been set to ‘apache’.
Request Transforms
Transforming requests can be achieved in a very similar way to transforming responses.
We still implement the ITransformProvider interface, but this time we’ll add a request transform within the public Apply method.
Here’s a contrived example where we get a Bearer token and attach it to the request before it is forwarded to the downstream service.
using System.Net.Http.Headers; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; using YarpDemo.Providers.Interfaces; namespace YarpDemo.Transforms; public class TokenRequestTransformProvider(ITokenProvider tokenProvider) : ITransformProvider {     public void ValidateRoute(TransformRouteValidationContext context)     {        // No custom validation required.     }    public void ValidateCluster(TransformClusterValidationContext context)     {         // No custom validation required.     }   public void Apply(TransformBuilderContext context)     {         context.AddRequestTransform(TransformRequest);     }    private async ValueTask TransformRequest(RequestTransformContext context)     {         if (!HttpMethods.IsPost(context.HttpContext.Request.Method))         {             return;         }         string? tenantId = null;         context.HttpContext.GetRouteModel().Config.Metadata?.             TryGetValue("TenantId", out tenantId);         if (string.IsNullOrWhiteSpace(tenantId))         {             context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;             return;         }         var token = await tokenProvider.GetTokenAsync(tenantId);         if (string.IsNullOrWhiteSpace(token))         {             context.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;             return;         }         context.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);     } }
Note that you’ll need to adjust the namespace to suit your project.
In the above example, we use a primary constructor to inject an ITokenProvider object instance, and the AddRequestTransform method is called within the Apply method. Notice that the private TransformRequest method is non-static, in contrast to the previous example, since an instance field needs to be accessed.
The transformation logic first makes sure that the HTTP request method is POST.
The tenantId is then determined by accessing the configured Metadata value for the route. Metadata configuration per route is a very useful feature of YARP and provides the means to treat routes differently depending on the configured values.
The code then demonstrates how to short-circuit a request by setting the HTTP Status Code on the response that is returned to the client. It is important to include a return statement after doing this to ensure that no other transformation logic is executed.
A token is retrieved using the ITokenProvider implementation and if this returns a non-empty value, it will be added as a Bearer token within the standard ‘Authorization’ header of the proxy request.
Note that this is a purely contrived example that aims to demonstrate how to access Metadata configuration, short-circuit requests, and add/set header values. It is not intended to be a recommendation on how to secure requests.
Don’t forget to register the Transform Provider in your Program.cs file before trying it out as per the previous example.
builder.Services.AddReverseProxy() Â Â Â Â .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) Â Â Â Â .AddTransforms<TokenRequestTransformProvider>() Â Â Â .AddTransforms<ServerResponseTransformProvider>();
The proxied request that the JSONPlaceholder server receives will now include the Bearer token within the Authorization header when issuing a POST request to the /todos endpoint.
Rewrite bodies
Another powerful thing you can do with YARP is to rewrite the body of the request before it is forwarded to the downstream service.
Here’s an example of how to do this.
using System.Text; using System.Text.Json.Nodes; using Yarp.ReverseProxy.Transforms; using Yarp.ReverseProxy.Transforms.Builder; namespace YarpDemo.Transforms; public class BodyRequestTransformProvider : ITransformProvider {     public void ValidateRoute(TransformRouteValidationContext context)     {        // No custom validation required.     }    public void ValidateCluster(TransformClusterValidationContext context)     {         // No custom validation required.     }     public void Apply(TransformBuilderContext context)     {         context.AddRequestTransform(TransformRequest);     }    private static async ValueTask TransformRequest(RequestTransformContext context)     {         var request = context.HttpContext.Request;         if (request.ContentLength is not > 0)         {             return;         }         var jsonNode = await JsonNode.ParseAsync(request.Body);         if (jsonNode is null)         {             return;         }         jsonNode["userId"] = 3;         var modifiedJson = jsonNode.ToJsonString();         var modifiedBytes = Encoding.UTF8.GetBytes(modifiedJson);         request.Body = new MemoryStream(modifiedBytes) { Position = 0 };         request.ContentLength = modifiedBytes.Length;         context.ProxyRequest.Content?.Headers.ContentLength = modifiedBytes.Length;     } }
In the above example, the TransformRequest method ensures that the content length for the request is greater than zero before proceeding.
The request body is then parsed asynchronously as a JsonNode object and the “userId” property is overridden to a value of 3.
Note that this is a rudimentary code sample for demonstration purposes. Additionally, exception handling should be added when parsing JSON to account for parsing errors or cancellations.
The code converts the JsonNode object to a JSON string and gets the encoded bytes. The request Body property is then set to a new MemoryStream consisting of the modified bytes, and the Position of the stream is reset, in case any transformation logic that is processed after this needs to reread the body.
Note that you may need to consider calling the EnableBuffering method on the request object and ensure the position of the body stream is at zero before attempting to read it, if you anticipate that more than one piece of transformation logic or middleware will need to read the body. Buffering requests in memory is not efficient and is not recommended for large requests.
Lastly, the content length header values are set to match the modified body length.
As per the previous examples, don’t forget to register the Transform Provider in your Program.cs file before trying it out.
builder.Services.AddReverseProxy() Â Â Â Â .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) Â Â Â Â .AddTransforms<TokenRequestTransformProvider>() Â Â Â Â .AddTransforms<BodyRequestTransformProvider>() Â Â Â Â .AddTransforms<ServerResponseTransformProvider>();
To test the body rewrite, you can issue a POST request to the YARP proxy as follows.
POST https://localhost:7101/todos Accept: application/json Content-Type: application/json {   "userId": 1,   "title": "Buy milk",   "completed": false }
The response that gets routed back from the JSONPlaceholder API should be similar to the following, reflecting the “userId” of 3 that the proxy set within the JSON request body.
{   "userId": 3,   "title": "Buy milk",   "completed": false,   "id": 201 }
Pretty cool, right?
Summary
In this post, I’ve walked through how to transform requests and responses using the YARP reverse proxy library.
I focused on Transform Providers and started by showing how you can inspect the headers on a proxy response and update the header values on the response that is returned to the client.
I then demonstrated how to retrieve Metadata that has been attached to configured routes, short-circuit requests by setting the response status code, use injected services, and set header values on the proxy request that is forwarded to downstream services.
Finally, I demonstrated how to rewrite request bodies and noted some of the downsides and nuances related to this.


Comments