A method was called at an unexpected time from IHttpFilter.SendRequestAsync

1

I'm developing an UWP app that calls a web service. For that I use a HttpClient object from Windows.Web.Http namespace and I pass a IHttpFilter object to its constructor. This filter is responsible for the authentication process. I based my solution following this link and the authentication logic is based on this

I don't know what I'm doing wrong but I got this exception: A method was called at an unexpected time. (Exception from HRESULT: 0x8000000E)

The scenario that I'm testing is when the token is invalid despite it is assumed it is valid. In this case the (now > TokenManager.Token.ExpiresOn) condition is false, then an authorization header is added (with an invalid token), then a request is sent, then the http response code and www-authenticate header is inspected and if it is neccessary, a new access token must be requested by means of refresh token in order to do a retry. It is when I reach this line when the exception is thrown (the second time): response = await InnerFilter.SendRequestAsync(request).AsTask(cancellationToken, progress);

I have no idea what I'm doing wrong. I have seen another questions where people got this error and usually it's because they try to get the task's result without waiting for the task's completion but I'm using the await keyword in all asynchronous methods.

public class AuthFilter : HttpFilter
{
    public override IAsyncOperationWithProgress<HttpResponseMessage, HttpProgress> SendRequestAsync(HttpRequestMessage request)
    {
        return AsyncInfo.Run<HttpResponseMessage, HttpProgress>(async (cancellationToken, progress) =>
        {
            var retry = true;
            if (TokenManager.TokenExists)
            {
                var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
                if (now > TokenManager.Token.ExpiresOn)
                {
                    retry = false;
                    await RefreshTokenAsync();
                }
                request.Headers.Authorization = new HttpCredentialsHeaderValue(TokenManager.Token.TokenType, TokenManager.Token.AccessToken);
            }

            HttpResponseMessage response = await InnerFilter.SendRequestAsync(request).AsTask(cancellationToken, progress);

            cancellationToken.ThrowIfCancellationRequested();

            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                var authHeader = response.Headers.WwwAuthenticate.SingleOrDefault(x => x.Scheme == "Bearer");
                if (authHeader != null)
                {
                    var challenge = ParseChallenge(authHeader.Parameters);
                    if (challenge.Error == "token_expired" && retry)
                    {
                        var success = await RefreshTokenAsync();
                        if (success)
                        {
                            request.Headers.Authorization = new HttpCredentialsHeaderValue(TokenManager.Token.TokenType, TokenManager.Token.AccessToken);
                            response = await InnerFilter.SendRequestAsync(request).AsTask(cancellationToken, progress);
                        }
                    }
                }
            }

            return response;
        });
    }

    private async Task<bool> RefreshTokenAsync()
    {
        using (var httpClient = new HttpClient())
        {
            httpClient.DefaultRequestHeaders.Accept.Add(new HttpMediaTypeWithQualityHeaderValue("application/json"));
            var content = new HttpStringContent(JsonConvert.SerializeObject(new { RefreshToken = TokenManager.Token.RefreshToken }), UnicodeEncoding.Utf8, "application/json");
            var response = await httpClient.PostAsync(SettingsService.Instance.WebApiUri.Append("api/login/refresh-token"), content);
            if (response.IsSuccessStatusCode)
                TokenManager.Token = JsonConvert.DeserializeObject<Token>(await response.Content.ReadAsStringAsync());
            return response.IsSuccessStatusCode;
        }
    }

    private (string Realm, string Error, string ErrorDescription) ParseChallenge(IEnumerable<HttpNameValueHeaderValue> input)
    {
        var realm = input.SingleOrDefault(x => x.Name == "realm")?.Value ?? string.Empty;
        var error = input.SingleOrDefault(x => x.Name == "error")?.Value ?? string.Empty;
        var errorDescription = input.SingleOrDefault(x => x.Name == "error_description")?.Value ?? string.Empty;
        return (realm, error, errorDescription);
    }

    public override void Dispose()
    {
        InnerFilter.Dispose();
        GC.SuppressFinalize(this);
    }
}

public abstract class HttpFilter : IHttpFilter
{
    public IHttpFilter InnerFilter { get; set; }
    public HttpFilter()
    {
        InnerFilter = new HttpBaseProtocolFilter();
    }
    public abstract IAsyncOperationWithProgress<HttpResponseMessage, HttpProgress> SendRequestAsync(HttpRequestMessage request);
    public abstract void Dispose();
}

EDIT:

According to these links (link, link), it seems to be that I cannot send the same request twice. But I need to re-send the same request but with different authentication header. How can I achieve that?

c#
web-services
uwp
async-await
asked on Stack Overflow Oct 25, 2017 by Wacho • edited Oct 28, 2017 by Wacho

1 Answer

2

Ok, after struggling with this for a long time, I ended up doing this:

public override IAsyncOperationWithProgress<HttpResponseMessage, HttpProgress> SendRequestAsync(HttpRequestMessage request)
{
    return AsyncInfo.Run<HttpResponseMessage, HttpProgress>(async (cancellationToken, progress) =>
    {
        var retry = true;
        if (TokenManager.TokenExists)
        {
            var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            if (now > TokenManager.Token.ExpiresOn)
            {
                retry = false;
                await RefreshTokenAsync();
            }
            request.Headers.Authorization = new HttpCredentialsHeaderValue(TokenManager.Token.TokenType, TokenManager.Token.AccessToken);
        }

        HttpResponseMessage response = await InnerFilter.SendRequestAsync(request).AsTask(cancellationToken, progress);

        cancellationToken.ThrowIfCancellationRequested();

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            var authHeader = response.Headers.WwwAuthenticate.SingleOrDefault(x => x.Scheme == "Bearer");
            if (authHeader != null)
            {
                var challenge = ParseChallenge(authHeader.Parameters);
                if (challenge.Error == "token_expired" && retry)
                {
                    var secondRequest = request.Clone();
                    var success = await RefreshTokenAsync();
                    if (success)
                    {
                        secondRequest.Headers.Authorization = new HttpCredentialsHeaderValue(TokenManager.Token.TokenType, TokenManager.Token.AccessToken);
                        response = await InnerFilter.SendRequestAsync(secondRequest).AsTask(cancellationToken, progress);
                    }
                }
            }
        }

        return response;
    });
}

public static HttpRequestMessage Clone(this HttpRequestMessage request)
{
    var clone = new HttpRequestMessage(request.Method, request.RequestUri)
    {
        Content = request.Content
    };
    foreach (KeyValuePair<string, object> prop in request.Properties.ToList())
    {
        clone.Properties.Add(prop);
    }
    foreach (KeyValuePair<string, string> header in request.Headers.ToList())
    {
        clone.Headers.Add(header.Key, header.Value);
    }

    return clone;
}

Because I needed to re-send the request, I made a second request cloning the first one.

answered on Stack Overflow Nov 3, 2017 by Wacho

User contributions licensed under CC BY-SA 3.0