There is a JSON-RPC API, which I'm currently implementing. It can be tested here.
The problem is that if an incorrect DTO model is passed to SendAsync<TResponse>, JsonSerializer.Deserialize is going to throw a JsonException, which is not handled by my code. I know I've got to use SetException in some way, but I don't know how to do it, so here is the question. The exception message should be printed in the console as well.
public sealed class Client : IDisposable
{
...
private readonly ConcurrentDictionary<long, IResponseHandler> _handlers = new();
...
public Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () =>
{
await foreach (var message in _client.Start(cancellationToken))
{
using var response = JsonDocument.Parse(message);
try
{
var requestId = response.RootElement.GetProperty("id").GetInt32();
// TODO: Handle JsonException errors via `SetException`?
// TODO: Show error when incorrect input parameters are filled
if (_handlers.ContainsKey(requestId))
{
_handlers[requestId].SetResult(message);
_handlers.TryRemove(requestId, out _);
}
}
catch (KeyNotFoundException)
{
// My point is that a message should be processed only if it doesn't include `id`,
// because that means that the message is an actual web socket subscription.
_messageReceivedSubject.OnNext(message);
}
}
}, cancellationToken);
...
return Task.CompletedTask;
}
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params
};
//var tcs = new TaskCompletionSource<TResponse>();
//_requestManager.Add(request.Id, request, tcs);
var handler = new ResponseHandlerBase<TResponse>();
_handlers[request.Id] = handler;
var message = JsonSerializer.Serialize(request);
_ = _client.SendAsync(message);
return handler.Task;
//return tcs.Task;
}
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params).ConfigureAwait(false);
return response.Result;
}
...
private interface IResponseHandler
{
void SetResult(string payload);
}
private class ResponseHandlerBase<TRes> : IResponseHandler
{
private readonly TaskCompletionSource<TRes> _tcs = new();
public Task<TRes> Task => _tcs.Task;
public void SetResult(string payload)
{
var result = JsonSerializer.Deserialize(payload, typeof(TRes));
_tcs.SetResult((TRes) result);
}
}
}
Coincidentally, I did something very similar while live-coding a TCP/IP chat application last week.
Since in this case you already have an IAsyncEnumerable<string> - and since you can get messages other than responses - I recommend also exposing that IAsyncEnumerable<string>:
public sealed class Client : IDisposable
{
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
// TODO: parse and handle responses for our requests
yield return message;
}
}
}
You can change this to be Rx-based if you want (_messageReceivedSubject.OnNext), but I figure if you already have IAsyncEnumerable<T>, then you may as well keep the same abstraction.
Then, you can parse and detect responses, passing along all other messages:
public sealed class Client : IDisposable
{
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null)
{
// TODO: handle
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
}
Then, you can define your collection of outstanding requests and handle messages that are for those requests:
public sealed class Client : IDisposable
{
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument>> _requests = new();
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null && _requests.TryRemove(requestId.Value, out var tcs))
{
tcs.TrySetResult(response);
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
}
Note the usage of ConcurrentDictionary.TryRemove, which is safer than accessing the value and then removing it.
Now you can write your general SendAsync. As I note in my video, I prefer to split up the code that runs synchronously in SendAsync and the code that awaits the response:
public sealed class Client : IDisposable
{
...
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params,
};
var tcs = new TaskCompletionSource<JsonDocument>(TaskCreationOptions.RunContinuationsAsynchronously);
_requests.TryAdd(request.Id, tcs);
return SendRequestAndWaitForResponseAsync();
async Task<TResponse> SendRequestAndWaitForResponseAsync()
{
var message = JsonSerializer.Serialize(request);
await _client.SendAsync(message);
var response = await tcs.Task;
return JsonSerializer.Deserialize(response, typeof(TResponse));
}
}
}
I've removed the "handler" concept completely, since it was just providing the type for JsonSerializer.Deserialize. Also, by using a local async method, I can use the async state machine to propagate exceptions naturally.
Then, your higher-level methods can be built on this:
public sealed class Client : IDisposable
{
...
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params);
return response.Result;
}
}
So the final code ends up being:
public sealed class Client : IDisposable
{
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument>> _requests = new();
public async IAsyncEnumerable<string> Start(CancellationToken cancellationToken)
{
await foreach (var message in _client.Start(cancellationToken))
{
var (requestId, response) = TryParseResponse(message);
if (requestId != null && _requests.TryRemove(requestId.Value, out var tcs))
{
tcs.TrySetResult(response);
}
else
{
yield return message;
}
}
(long? RequestId, JsonDocument? Response) TryParseResponse(string message)
{
try
{
var document = JsonDocument.Parse(message);
var requestId = response.RootElement.GetProperty("id").GetInt32();
return (document, requestId);
}
catch
{
return (null, null);
}
}
}
public Task<TResponse> SendAsync<TResponse>(string method, object #params)
{
var request = new JsonRpcRequest<object>
{
JsonRpc = "2.0",
Id = NextId(),
Method = method,
Params = #params,
};
var tcs = new TaskCompletionSource<JsonDocument>(TaskCreationOptions.RunContinuationsAsynchronously);
_requests.TryAdd(request.Id, tcs);
return SendRequestAndWaitForResponseAsync();
async Task<TResponse> SendRequestAndWaitForResponseAsync()
{
var message = JsonSerializer.Serialize(request);
await _client.SendAsync(message);
var response = await tcs.Task;
return JsonSerializer.Deserialize(response, typeof(TResponse));
}
}
public async Task<AuthResponse?> AuthenticateAsync(string clientId, string clientSecret)
{
var #params = new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", clientId},
{"client_secret", clientSecret}
};
var response = await SendAsync<SocketResponse<AuthResponse>>("public/auth", #params);
return response.Result;
}
}
You may also want to check out David Fowler's Project Bedrock, which may simplify this code quite a bit.
Related
I am making a notification microservice for users. It is impossible to use the SignalR built into MassTransit due to the built-in microservice architecture.
After accepting messages from the queue RabbitMq, it is called StartAsync:
public class MassTransitManager : IMassTransitManager
{
public MassTransitManager(Func<string, Task<string>> processDelegateAsync)
{
ProcessDelegateAsync = processDelegateAsync;
}
public Func<string, Task<string>> ProcessDelegateAsync { get; set; }
public async Task<string> StartAsync(string requestString)
{
string response;
try
{
response = await ProcessMessageAsync(requestString);
}
catch (Exception exception)
{
response = exception.ToString();
}
return response;
}
private async Task<string> ProcessMessageAsync(string requestString)
{
if (ProcessDelegateAsync == null)
throw new InvalidOperationException($"Not found delegate {nameof(ProcessDelegateAsync)}.");
var result = await ProcessDelegateAsync(requestString);
return result;
}
}
MassTransitManager is in Class Library. Delegate created in Startup:
services.AddSingleton<IMassTransitManager, MassTransitManager>(provider =>
{
return new MassTransitManager(async serializedMessage =>
{
var hubContext = provider.GetRequiredService<IHubContext<NoticeHub>>();
await hubContext.Clients.All.SendAsync("SendResponse", "Hi all.");
return "Hi man";
});
});
But no client receives this message. What can be the ways to solve this problem?
I need to sent custom exceptions message to client.
I have the following code:
in Startup.cs ConfigureServices method
services.AddGrpc(options => options.Interceptors.Add<ErrorInterceptor>());
in ErrorInterceptor.cs
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (ValidationException validationExc)
{
await WriteResponseHeadersAsync(StatusCode.InvalidArgument, translation =>
translation.GetEnumTranslation(validationExc.Error, validationExc.Parameters));
}
catch (Exception)
{
await WriteResponseHeadersAsync(StatusCode.Internal, translation =>
translation.GetEnumTranslation(HttpStatusCode.InternalServerError));
}
return default;
Task WriteResponseHeadersAsync(StatusCode statusCode, Func<ITranslationService, string> getMessage)
{
var httpContext = context.GetHttpContext();
var translationService = httpContext.RequestServices.GetService<ITranslationService>();
var errorMessage = getMessage(translationService);
var responseHeaders = new Metadata
{
{ nameof(errorMessage) , errorMessage },//1) can see in browser's devTools, but not in the code
{ "content-type" , errorMessage },//2) ugly, but works
};
context.Status = new Status(statusCode, errorMessage);//3) not working
return context.WriteResponseHeadersAsync(responseHeaders);//4) alternative?
}
}
in mask-http.service.ts
this.grpcClient.add(request, (error, reply: MaskInfoReply) => {
this.grpcBaseService.handleResponse<MaskInfoReply.AsObject>(error, reply, response => {
const mask = new Mask(response.id, response.name);
callback(mask);
});
});
in grpc-base.service.ts
handleResponse<T>(error: ServiceError,
reply: {
toObject(includeInstance?: boolean): T;
},
func: (response: T) => void) {
if (error) {
const errorMessage = error.metadata.headersMap['content-type'][0];
this.toasterService.openSnackBar(errorMessage, "Ok");
console.error(error);
return;
}
const response = reply.toObject();
func(response);
}
I wanted to send error using Status (comment 3), but it doesn't get changed
I wonder if there is an alternative way to send it not in response headers (comment 4)
I tried to add custom response header (comment 1), but the only one I received in client code was 'content-type' so I decided to overwrite it (comment 2)
I hit the same dead end recently and decided to do it this way:
Create an error model:
message ValidationErrorDto {
// A path leading to a field in the request body.
string field = 1;
// A description of why the request element is bad.
string description = 2;
}
message ErrorSynopsisDto {
string traceTag = 1;
repeated ValidationErrorDto validationErrors = 2;
}
Create an extension for the error model that serializes the object to JSON:
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public static class ErrorSynopsisDtoExtension
{
public static string ToJson(this ErrorSynopsisDto errorSynopsisDto) =>
JsonConvert.SerializeObject(
errorSynopsisDto,
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
Create a custom exception that encapsulates error model:
public class OperationException : Exception
{
private readonly List<ValidationErrorDto> validationErrors = new();
public bool HasValidationErrors => this.validationErrors.Count > 0;
public OperationException(string traceTag) : base
(
new ErrorSynopsisDto
{
TraceTag = traceTag
}.ToJson() // <- here goes that extension
) => ErrorTag = traceTag;
public OperationException(
string traceTag,
List<ValidationErrorDto> validationErrors
) : base
(
new ErrorSynopsisDto
{
TraceTag = traceTag,
ValidationErrors = { validationErrors }
}.ToJson() // <- here goes that extension again
)
{
ErrorTag = traceTag;
this.validationErrors = validationErrors;
}
}
Throw custom exception from service call handlers:
throw new OperationException(
"MY_CUSTOM_VALIDATION_ERROR_CODE",
// the following block can be simplified with a mapper, for reduced boilerplate
new()
{
new()
{
Field = "Profile.FirstName",
Description = "Is Required."
}
}
);
And lastly, the exception interceptor:
public class ExceptionInterceptor : Interceptor
{
private readonly ILogger<ExceptionInterceptor> logger;
public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger) => this.logger = logger;
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation
)
{
try
{
return await continuation(request, context);
}
catch (OperationException ex)
{
this.logger.LogError(ex, context.Method);
var httpContext = context.GetHttpContext();
if (ex.HasValidationErrors)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
else
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, context.Method);
var httpContext = context.GetHttpContext();
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var opEx = new OperationException("MY_CUSTOM_INTERNAL_ERROR_CODE");
throw new RpcException(
new Status(
StatusCode.Internal,
opEx.Message
)
);
}
}
}
On the TypeScript-based frontend, I simply catch RPC errors and hydrate the message like this:
JSON.parse(err.message ?? {}) as ErrorSynopsisDto
After publishing the bot, the user needs to chat with the bot again first in order send a proactive message. I followed this sample but instead of storing the conversation reference in the variable, I stored it in cosmosDB. However I still can't send proactive message if the user has not chatted with the bot after publishing. Is there a way to send proactive message even when the user has not chatted with the bot after publishing?
DialogBot
private async void AddConversationReference(ITurnContext turnContext)
{
var userstate = await _userProfileAccessor.GetAsync(turnContext, () => new BasicUserState());
userstate.SavedConversationReference = turnContext.Activity.GetConversationReference();
_conversationReferences.AddOrUpdate(userstate.SavedConversationReference.User.Id, userstate.SavedConversationReference, (key, newValue) => userstate.SavedConversationReference);
}
protected override Task OnConversationUpdateActivityAsync(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
AddConversationReference(turnContext);
return base.OnConversationUpdateActivityAsync(turnContext, cancellationToken);
}
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
Logger.LogInformation("Running dialog with Message Activity.");
AddConversationReference(turnContext);
await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
}
api/notify
namespace SabikoBotV2.Controllers
{
[Route("api/notify")]
public class NotifyController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly string _appId;
private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;
private readonly IStatePropertyAccessor<BasicUserState> _userProfileAccessor;
public NotifyController(UserState userState, IBotFrameworkHttpAdapter adapter, ICredentialProvider credentials, ConcurrentDictionary<string, ConversationReference> conversationReferences)
{
_userProfileAccessor = userState.CreateProperty<BasicUserState>("UserProfile");
_adapter = adapter;
_conversationReferences = conversationReferences;
_appId = ((SimpleCredentialProvider)credentials).AppId;
if (string.IsNullOrEmpty(_appId))
{
_appId = Guid.NewGuid().ToString(); //if no AppId, use a random Guid
}
}
public async Task<IActionResult> Get()
{
try
{
foreach (var conversationReference in _conversationReferences.Values)
{
await ((BotAdapter)_adapter).ContinueConversationAsync(_appId, conversationReference, BotCallback, default(CancellationToken));
}
return new ContentResult()
{
Content = "<html><body><h1>Proactive messages have been sent.</h1></body></html>",
ContentType = "text/html",
StatusCode = (int)HttpStatusCode.OK,
};
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
private async Task BotCallback(ITurnContext turnContext, CancellationToken cancellationToken)
{
var userstate = await _userProfileAccessor.GetAsync(turnContext, () => new BasicUserState(), cancellationToken);
if (userstate.SavedConversationReference.ServiceUrl != null && userstate.SavedConversationReference.ServiceUrl != string.Empty)
{
MicrosoftAppCredentials.TrustServiceUrl(userstate.SavedConversationReference.ServiceUrl);
}
else if (turnContext.Activity.ServiceUrl != null && turnContext.Activity.ServiceUrl != string.Empty)
{
MicrosoftAppCredentials.TrustServiceUrl(turnContext.Activity.ServiceUrl);
}
else
{
MicrosoftAppCredentials.TrustServiceUrl("https://facebook.botframework.com/");
}
if(userstate.Reminders != null)
{
foreach (var reminder in userstate.Reminders)
{
if (reminder.DateAndTime.TrimMilliseconds() == DateTimeNowInGmt().TrimMilliseconds())
{
var timeProperty = new TimexProperty(reminder.DateAndTimeTimex);
var naturalDate = timeProperty.ToNaturalLanguage(DateTimeNowInGmt().TrimMilliseconds());
await turnContext.SendActivityAsync($"It's {DateTimeNowInGmt().ToLongDateString()}. \n\nReminding you to {reminder.Subject}.");
}
}
}
}
public static DateTime DateTimeNowInGmt()
{
TimeZoneInfo phTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time");
var t = DateTime.Now;
DateTime phTime = TimeZoneInfo.ConvertTime(t, phTimeZone);
return phTime;
}
}
}
public class RollingRequests
{
private const int DefaultNumSimultaneousRequests = 10;
private readonly HttpClient _client; // Don't worry about disposing see https://stackoverflow.com/questions/15705092/do-httpclient-and-httpclienthandler-have-to-be-disposed
private readonly HttpCompletionOption _httpCompletionOption;
private readonly int _numSimultaneousRequests;
public RollingRequests() : this(DefaultNumSimultaneousRequests)
{
}
public RollingRequests(int windowSize) : this(new HttpClient(), windowSize)
{
}
public RollingRequests(HttpClient client, int numSimultaneousRequests, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
{
_client = client;
_numSimultaneousRequests = numSimultaneousRequests;
_httpCompletionOption = httpCompletionOption;
}
public async Task ExecuteAsync(List<string> urls, CancellationToken cancellationToken, Action<HttpResponseHeaders, string> requestCallback = null)
{
var nextIndex = 0;
var activeTasks = new List<Task<Tuple<string, HttpResponseMessage>>>();
var startingIndex = Math.Min(_numSimultaneousRequests, urls.Count);
for (nextIndex = 0; nextIndex < startingIndex; nextIndex++)
{
activeTasks.Add(RequestUrlAsync(urls[nextIndex], cancellationToken));
}
while (activeTasks.Count > 0)
{
var finishedTask = await Task.WhenAny(activeTasks).ConfigureAwait(false);
activeTasks.Remove(finishedTask);
var retryUrl = await ProcessTask(await finishedTask, requestCallback).ConfigureAwait(false);
// If retrying, add the URL to the end of the queue
if (retryUrl != null)
{
urls.Add(retryUrl);
}
if (nextIndex < urls.Count)
{
activeTasks.Add(RequestUrlAsync(urls[nextIndex], cancellationToken));
nextIndex++;
}
}
}
private async Task<string> ProcessTask(Tuple<string, HttpResponseMessage> result, Action<HttpResponseHeaders, string> requestCallback = null)
{
var url = result.Item1;
using (var response = result.Item2)
{
if (!response.IsSuccessStatusCode)
{
return url;
}
if (requestCallback != null)
{
string content = null;
if (_httpCompletionOption == HttpCompletionOption.ResponseContentRead)
{
content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
requestCallback(response.Headers, content);
}
return null;
}
}
private async Task<Tuple<string, HttpResponseMessage>> RequestUrlAsync(string url, CancellationToken ct)
{
var response = await _client.GetAsync(url, _httpCompletionOption, ct).ConfigureAwait(false);
return new Tuple<string, HttpResponseMessage>(url, response);
}
}
This is a class that allows for X simultaneous requests to be on-going at once. When I am unit-testing this class and I moq the HttpClient giving each request a 1 second delay the initial activeTasks.Add is taking 5 seconds if I have 5 requests, suggesting to me that RequestUrlAsync isn't truly async.
Can anyone spot the issue?
Edit:
This is how I am sleeping the mocked client
_messageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(MethodToMoq, ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.Callback(() => Thread.Sleep(1000))
.ReturnsAsync(callback)
.Verifiable();
I tested your class RollingRequests with actual urls and works as expected. Then I replaced await _client.GetAsync(... with await Task.Delay(1000) and continued working as expected. Then replaced the same line with Thread.Sleep(1000) and replicated your problem.
Moral lesson: avoid blocking the current thread when running asynchronous code!
(It would be easier to answer if you had provided a Minimal, Reproducible Example)
Mixing Thread.Sleep with asynchronous code isn't a good idea because it is a blocking call.
Mocking internals should also be avoided.
Here's a simple example of a test that takes about 1 second to execute 10 requests:
async Task Test()
{
var httpClient = new HttpClient(new TestHttpMessageHandler());
var ticks = Environment.TickCount;
await Task.WhenAll(Enumerable.Range(0, 10).Select(_ => httpClient.GetAsync("https://stackoverflow.com/")));
Console.WriteLine($"{Environment.TickCount - ticks}ms");
}
class TestHttpMessageHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await Task.Delay(1000);
return new HttpResponseMessage();
}
}
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?
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.