reverse-proxy icon indicating copy to clipboard operation
reverse-proxy copied to clipboard

Ability to enable StreamCopyHttpContent when body is empty

Open arminbashizade opened this issue 2 years ago • 8 comments
trafficstars

What should we add or change to make your life better?

(somehow similar to #618) provide ability to enable copying content when the request body is originally empty, but will be populated later

Why is this important to you?

I have a transformer that adds a dynamic attribute (different for each request) to body for some routes, and I follow instructions outlined in #1601:

// deserialize, add attribute, serialize into bodyStr
var bytes = Encoding.UTF8.GetBytes(bodyStr);
context.HttpContext.Request.Body = new MemoryStream(bytes);
context.ProxyRequest.Content!.Headers.ContentLength = bytes.Length;

for example:

{
  "attr1": "value1"
}
// becomes
{
  "attr1": "value1",
  "attr2": "dynamic value"
}
////////////////
{}
// becomes
{
  "attr2": "dynamic value"
}
////////////////

// also becomes
{
  "attr2": "dynamic value"
}

this works fine when body is not empty, and by that I mean ContentLength == 0, however when the body is empty ProxyRequest.Content is set to null in HttpForwarder here: https://github.com/microsoft/reverse-proxy/blob/b6f00851fc1032706c275b414244a95ef70d3a26/src/ReverseProxy/Forwarder/HttpForwarder.cs#L561-L584

and it's too late to set ContentLength or override IHttpRequestBodyDetectionFeature because transformers are applied after that: https://github.com/microsoft/reverse-proxy/blob/b6f00851fc1032706c275b414244a95ef70d3a26/src/ReverseProxy/Forwarder/HttpForwarder.cs#L417-L423

I guess I can directly set context.ProxyRequest.Content in the specific case where body is empty (can you please confirm there's no risk here?) but I'd rather not adding an exception case. Is this something I can achieve with existing implementation?

arminbashizade avatar Mar 31 '23 19:03 arminbashizade

I guess I can directly set context.ProxyRequest.Content in the specific case where body is empty (can you please confirm there's no risk here?) but I'd rather not adding an exception case. Is this something I can achieve with existing implementation?

No, you should not replace the Content with your own, YARP has a custom implementation that it relies on.

Have you tried setting HttpContext.Request.Body in a middleware before calling into YARP? E.g.

endpoints.MapReverseProxy(app =>
{
    // app.UseSessionAffinity();
    // app.UseLoadBalancing();
    // app.UsePassiveHealthChecks();

    app.Use(async (context, next) =>
    {
        context.Request.Body = ...;
        await next();
    });
});

MihaZupan avatar Mar 31 '23 19:03 MihaZupan

I understand that YARP has its own implementation, if I'm following the code correctly it's SetupRequestBodyCopy, but in this specific case, YARP will set content to null here: https://github.com/microsoft/reverse-proxy/blob/b6f00851fc1032706c275b414244a95ef70d3a26/src/ReverseProxy/Forwarder/HttpForwarder.cs#L419-L420 because SetupRequestBodyCopy returns null, so is that still going to be an issue if I overwrite context.ProxyRequest.Content?

arminbashizade avatar Mar 31 '23 19:03 arminbashizade

so is that still going to be an issue if I overwrite context.ProxyRequest.Content?

Yes. YARP will assume that it's not sending any request content, so you will run into a bunch of issues:

  • YARP relies on the custom implementation for telemetry and error detection.
  • YARP may not wait for the request content upload to complete, which would lead to a bunch of thread-safety issues as we're telling ASP.NET that we're done with a request while we're still reading the body in the background.
    • For example https://github.com/microsoft/reverse-proxy/blob/b6f00851fc1032706c275b414244a95ef70d3a26/src/ReverseProxy/Forwarder/HttpForwarder.cs#L300-L307

MihaZupan avatar Mar 31 '23 19:03 MihaZupan

Thanks for providing extra context, I'll give the middleware a shot.

arminbashizade avatar Mar 31 '23 20:03 arminbashizade

I was able to create a middleware that can enable modifying the body

app.MapReverseProxy(x =>
{
    x.Use(async (context, next) =>
    {
        var canHaveBodyFeature = context.Features.Get<IHttpRequestBodyDetectionFeature>();
        if (HttpMethods.IsPost(context.Request.Method) && context.Request.ContentLength == 0 && canHaveBodyFeature?.CanHaveBody != true)
        {           
            context.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
        }
        await next();
    });
});

danhulet avatar Apr 04 '23 17:04 danhulet

@MihaZupan if you don't see any issues with the solution @danhulet provided, feel free to close this.

arminbashizade avatar Apr 04 '23 20:04 arminbashizade

Ah, we modified the custom content object to delay capturing the request body stream for cases like this, but that only works if we thought there was a body to begin with. There's not an easy way to add a body to a request that didn't have one. We should spend a few minutes to see if there's a way to make that work.

Tratcher avatar Apr 04 '23 21:04 Tratcher

And if not, it'd be good to add a cheap check in YARP to scream if you change the content object, as you may be unlucky and see it "work" in simple tests.

MihaZupan avatar Apr 04 '23 21:04 MihaZupan