I have a standard form made with Html.BeginForm that posts to an async action in a controller. It looks like this (this is an outline, not the actual code):
[HttpPost]
public async Task<ActionResult> Index(UserCreds creds)
{
try
{
if (ModelState.IsValid)
{
var user = await loginRep.Login(creds.Username, creds.Password);
if (user != null)
{
_context.SetAuthenticationToken(user);
return RedirectToAction("Index", "Landing");
}
else
{
ModelState.AddModelError("", "Login failed.");
}
}
else
{
_logger.Debug(string.Format("User failed authentication."));
}
}
catch (Exception ex)
{
throw new HttpException(500, string.Format("An error occured during the execution of the action {0} from Controller {1}", "Index", "Login"), ex);
}
return View();
}
Upon submitting the form for the first time, the browser will be awaiting indefinitely for a response, even though one can see stepping with the debugger that the RedirectToAction is reached. If one then stops the request in the browser, and simply submit again, the redirect happens. All subsequent attempts to login also redirect successfully. The authentication token is also somehow not set during that first attempt.
This likely has something to do with delegate usage inside loginRep.Login
. Inside it eventually does something like this:
private async Task<LoginResponse> SendLoginRequest(string username, string password)
{
TaskCompletionSource<LoginResponse> tcs = new TaskCompletionSource<LoginResponse>();
LoginResponseCallback callback = null;
callback = new LoginResponseHandler(delegate (response) {
securityService.OnLoginResponse -= callback;
tcs.SetResult(response);
});
securityService.OnLoginResponse += callback;
securityService.SendLoginRequest(username, password);
return await tcs.Task;
}
Does anyone understand what is happening? If it's a deadlock, I wouldn't have expected to see the debugger reach the redirect, nor would I have expected the login to work for all attempts other than the first.
Note the form does work the first time if one just skips sending a login request and just hardcode what a successful response would look like.
Ok. The issue was resolved. There was nothing wrong with the two code samples I showed. The error was with setting up my security service, so it was unfortunately rather specific to this application.
That said, the infinite waiting happened because the Application_Error in Global.asax.cs was effectively swallowing certain exceptions. Once I changed it to always redirect to my error page no matter what, at least it immediately redirected to the error page when the issue happened, instead of hanging from the user's perspective.