Software Development Simplified

How to Replace YARP Responses: Transforming Status Codes for Better Error Handling

By Dario on May 3, 2025
YARP Response Transformation

How to Replace YARP Responses: Transforming Status Codes for Better Error Handling

Introduction

When building modern applications with microservices architecture, one common challenge is ensuring consistent error handling across different services. Sometimes, upstream services return “successful” HTTP 200 responses containing validation errors in the response body - a pattern that can make error handling in client applications unnecessarily complex.

Today, I’ll show you how to use YARP (Yet Another Reverse Proxy) to intercept these responses and transform them into more appropriate HTTP status codes. Specifically, we’ll look at a real-world example where we convert a 200 response containing validation errors into a 422 Unprocessable Entity response, making it more semantically correct and easier for clients to handle.

Note: This example works with YARP version 2.10 and later, which introduces improved response transformation capabilities. You can find the complete working example in my GitHub repository.

Understanding YARP Response Transforms

YARP is a reverse proxy library developed by Microsoft that provides powerful capabilities for routing, load balancing, and transforming requests and responses. One of its key features is the ability to intercept and modify responses from upstream services before they reach the client.

Response transforms in YARP allow you to:

  • Modify response headers
  • Change status codes
  • Transform response bodies
  • Add or remove cookies
  • And much more

These transforms are applied through a pipeline, making it easy to chain multiple transformations together.

The Problem: 200 Responses with Validation Errors

Consider this scenario: your backend service performs validation on submitted data and returns a JSON response like this:

{
  "success": false,
  "errors": ["Invalid first name", "Invalid last name"]
}

The problem? This response comes with a 200 OK status code, even though it clearly represents a validation failure. According to HTTP semantics, a more appropriate status code would be 422 Unprocessable Entity.

The Solution: Creating a YARP Response Transform

Let’s look at how we can create a response transform to detect this pattern and convert it to a proper 422 response. We’ll utilize YARP’s powerful response transformation capabilities introduced in version 2.10 and later.

All the code shown in this post is available in my GitHub repository as a fully functional example.

Step 1: Define the Validation Response Model

First, let’s create a class that represents the validation response structure we expect to receive:

namespace YourApplication.Features.ApiTransformations;

/// <summary>
/// Represents the response from the validation endpoint
/// </summary>
public class ValidationResponse
{
    /// <summary>
    /// Indicates whether the validation was successful
    /// </summary>
    public bool Success { get; set; }
    
    /// <summary>
    /// List of error messages, only populated when Success is false
    /// </summary>
    public string[]? Errors { get; set; }
}

Step 2: Create a Response Transform Provider

Next, let’s create a transform provider that will be registered with YARP:

using System.Text;
using System.Text.Json;
using Yarp.ReverseProxy.Transforms;
using Yarp.ReverseProxy.Transforms.Builder;

namespace YourApplication.Features.ApiTransformations;

internal static class ValidationErrorTransformer
{
    public static void Transform(TransformBuilderContext builder) =>
        builder.AddResponseTransform(async context =>
        {
            HttpContext httpContext = context.HttpContext;
            HttpResponse response = httpContext.Response;
            
            // Only process responses with 200 status code
            if (response.StatusCode != 200)
            {
                return;
            }
            
            try
            {
                // Get the response content as a string
                string responseBody = string.Empty;
                
                if (context.ProxyResponse is not null)
                {
                    responseBody = await context.ProxyResponse.Content.ReadAsStringAsync();
                    
                    // Try to deserialize the response directly into our expected format
                    try 
                    {
                        ValidationResponse? validationResponse = JsonSerializer.Deserialize<ValidationResponse>(
                            responseBody, 
                            new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
                        );
                        
                        // Check if this is a validation error response
                        if (validationResponse is { Success: false, Errors.Length: > 0 })
                        {
                            // Change status code to 422 Unprocessable Entity
                            response.StatusCode = 422;
                            
                            // Replace the response body with only the array of errors
                            context.SuppressResponseBody = true;
                            
                            // Serialize just the errors array
                            string errorsJson = JsonSerializer.Serialize(validationResponse.Errors, 
                                new JsonSerializerOptions
                                {
                                    WriteIndented = true
                                });
                            
                            // Set content type and write the new response
                            response.ContentType = "application/json";
                            response.ContentLength = Encoding.UTF8.GetByteCount(errorsJson);
                            await response.WriteAsync(errorsJson);
                        }
                    }
                    catch (JsonException)
                    {
                        // If we can't deserialize to our expected format, it's not a validation error response
                    }
                }
            }
            catch (Exception)
            {
                // In case of errors, return a 500 Internal Server Error
                response.StatusCode = 500;
            }
        });
}

Step 3: Register the Transform with YARP

Next, we register this transform in our application’s startup code:

public class Program
{
    public static void Main(string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

        // Add YARP reverse proxy services
        builder.Services.AddReverseProxy()
            .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
            .AddTransforms(transformBuilder =>
            {
                // Add our validation error transformer
                ValidationErrorTransformer.Transform(transformBuilder);
            });

        WebApplication app = builder.Build();

        // Configure the HTTP request pipeline
        if (app.Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        
        // Map the reverse proxy endpoints
        app.MapReverseProxy();

        app.Run();
    }
}

Step 4: Configure YARP Routes

Finally, configure your YARP routes in appsettings.json to use this transform:

{
  "ReverseProxy": {
    "Routes": {
      "api": {
        "ClusterId": "apiCluster",
        "Match": {
          "Path": "/api/{**catch-all}"
        }
      }
    },
    "Clusters": {
      "apiCluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://your-upstream-api.com/"
          }
        }
      }
    }
  }
}

How It Works

Let’s break down how this transform works:

  1. We add a response transform to the YARP pipeline that intercepts all responses from upstream services.
  2. For each response, we check if it has a 200 status code, which is the condition we’re looking for.
  3. We read the response body from context.ProxyResponse.Content, which is available in YARP 2.10+.
  4. We deserialize the response into our ValidationResponse model with case-insensitive property matching.
  5. Using a pattern matching check (validationResponse is { Success: false, Errors.Length: > 0 }), we determine if it’s a validation error.
  6. If it is, we change the status code to 422 and replace the response body with just the array of error messages.

The key improvement in this version is the direct access to context.ProxyResponse.Content, which eliminates the need for manual body manipulation used in older versions of YARP.

For a fully working implementation with tests, check out my GitHub repository.

Key Considerations

Performance

Reading and transforming response bodies can impact performance, so consider the following:

  • Response Size: For large responses, memory usage may increase.
  • Content Encoding: This example assumes uncompressed responses. For compressed responses (gzip, br), you’ll need to handle decompression and compression.
  • Caching: If your application uses response caching, transformed responses will need appropriate cache headers.

Error Handling

The example includes basic error handling, but in a production system you might want to:

  • Have more granular error responses
  • Consider fallback strategies for different failure modes

Content Type Handling

Our example assumes JSON responses. For other content types, you’ll need different parsing strategies.

Advanced Scenarios

Selective Transformation

You might want to apply this transform only for certain routes or endpoints. You can modify the transform to check the request path or add route-specific configuration:

// Only transform responses from certain paths
if (!httpContext.Request.Path.StartsWithSegments("/api/users"))
{
    return;
}

Combining with Other Transforms

YARP allows you to chain multiple transforms. You might want to combine this with:

  • Request transforms that add headers or modify the request
  • Response header transforms
  • Authentication or authorization transforms

Dynamic Transformation Rules

For more complex scenarios, you could load transformation rules from configuration or a database:

// Example: Load transformation rules from configuration
var transformRules = httpContext.RequestServices.GetRequiredService<IOptions<TransformRules>>().Value;
if (!transformRules.EnableValidationTransform)
{
    return;
}

Conclusion

YARP’s response transforms provide a powerful way to ensure consistent error handling across your microservices architecture. By intercepting and modifying responses as demonstrated, you can create a more consistent API experience that follows HTTP semantics and makes error handling easier for client applications.

The specific example we explored - transforming a 200 response with validation errors into a 422 status code - is just one of many possibilities. The same approach can be used for various scenarios where upstream services don’t follow your desired API conventions.

Remember that while response transformation is powerful, it’s also important to consider performance implications, especially for high-traffic applications. When possible, working with upstream service teams to standardize error handling is often the best long-term solution.

For a complete implementation of this example and more, visit my GitHub repository.

Resources


Have you used YARP for other interesting response transformations? Let me know in the comments!

Twitter iconLinkedIn iconGitHub iconYouTube icon
© 2025 Dario Griffo. All rights reserved.