Search code examples
c#httprequestdotnet-httpclientpolly

HttpRequestMessage.SetPolicyExecutionContext does not pass context to policy


This is about policies and contexts using the Polly library (v7.2.3) and Microsoft.Extensions.Http.Polly (v7.0.5). I'm confused about the SetPolicyExecutionContext method of the HttpRequestMessage class: I know it works when policies are added to the HttpClient via an HttpClientFactory, but I'm using it in a case where the HttpClient does not have any policies attached and it's not working as I was expecting. The context is to be used later in the policy OnRetry delegate, like so:

OnRetry = (_, _, _, context) => {
    var value = context.ContainsKey("X") ? (string)context["X"] : "NOT FOUND";
    Debug.WriteLine(value); 
},

The HttpRequest is sent using the ExecuteAsync method of the policy (after setting the context):

//Case 1 (Context in Request)
var context = new Context();
context.Add("X", "ABC");
request.SetPolicyExecutionContext(context);
policy.ExecuteAsync(() => _httpClient.SendAsync(request));

When this code executes and the web request fails, the OnRetry handler always writes "NOT FOUND" to the output.

But if the context is passed directly to the policy, like this:

//Case 2 (Context in Policy)
var context = new Context();
context.Add("myKey", "ABC");
policy.ExecuteAsync((token) => _httpClient.SendAsync(request), context);

Then it works as expected: on failed requests the output gets "ABC".

Why is that? I thought that in Case 1 the ExecuteAsync method would get the context from the request and pass it to the executing policy so it would be equivalent to Case 2, but that's not happening, so it looks like I'm missing something.


Solution

  • The SetPolicyExecutionContext simply does the following:

    request.Properties["PolicyExecutionContext"] = pollyContext;
    

    which means after this call you can access the context via the HttpRequestMessage's Properties collection or simply by calling the request.GetPolicyExecutionContext().

    So, it attaches a context to the request object NOT to the execution.

    If you want to access the context inside the OnRetry{Async} delegate then you have to explicitly pass the context to the Execute{Async} call.


    UPDATE #1: A bit more details

    Whenever you use the AddHttpClient with the AddPolicyHandler then the context propagation is done on your behalf via the PolicyHttpMessageHandler. This class is a DelegatingHandler and its SendAsync method does the magic for you:

    var cleanUpContext = false;
    var context = request.GetPolicyExecutionContext();
    if (context == null)
    {
        context = new Context();
        request.SetPolicyExecutionContext(context);
        cleanUpContext = true;
    }
    
    HttpResponseMessage response;
    try
    {
        var policy = _policy ?? SelectPolicy(request);
        response = await policy.ExecuteAsync((c, ct) => SendCoreAsync(request, c, ct), context, cancellationToken).ConfigureAwait(false);
    }
    finally
    {
        if (cleanUpContext)
        {
            request.SetPolicyExecutionContext(null);
        }
    }
    
    return response;
    
    1. It tries to retrieve the context from the request
    2. If it does not present then it will create a new temporarily one
    3. It passes the context explicitly to the ExecuteAsync
    4. If there was no context attached to the request at the time of the SendAsync call then it will delete the temporarily attached context

    Based on your question I assume that you are not using the PolicyHttpMessageHandler so, no one will automatically pass the context to the ExecuteAsync.