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.
Related
I'm working on a Core 3.1 Web API and an MVC application that uses it. In the MVC app I have UserRepo set up containing methods that send requests to the API:
public class UserRepo : IUserRepo
{
private readonly IHttpClientFactory _clientFactory;
public UserRepo(IHttpClientFactory httpClientFactory)
{
_clientFactory = httpClientFactory;
}
public async Task<User> GetById(int Id)
{
// same code structure as Update ...
}
public async Task<User> Update(User user)
{
HttpClient client = _clientFactory.CreateClient("NamedClient");
try
{
HttpResponseMessage response = await client.PutAsync($"api/Users/{user.Id}", ContentEncoder.Encode(user));
return await response.Content.ReadFromJsonAsync<User>();
}
catch (Exception ex)
{
throw;
}
}
public async Task<User> Insert(User user)
{
// same code structure as Update ...
}
}
The Update method never throws errors like 400, 404, etc, that come back from the API, resulting in silent errors. I found that to cause exceptions I need to call response.EnsureSuccessStatusCode();, which worked.
However, the exception doesn't contain what I need to find out what went wrong with the API call. If a 400 error occurs, an exception will be thrown saying that 400 error occurred, but not why it occurred. The why is returned to the response variable and it may look something like this due to validation I have implemented:
{
"errors": {
"FirstName": [
"The FirstName field is required."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|502d647b-4c7425oa321c8c7b."
}
Is there a widely used way to handle the response that comes back after an error is produced in the API? I want to know why a 400 error occurred so I know what to fix. I just don't know what is the "right" way to handle these response messages.
One idea I had was to catch the exception and log it along with the response text every time before throwing it. Then when my app crashes I can go to the logs and read the message returned. The Update method would look like this:
public async Task<User> Update(User user)
{
HttpClient client = _clientFactory.CreateClient("NamedClient");
HttpResponseMessage response = await client.PutAsync($"api/Users/{user.Id}", ContentEncoder.Encode(user));
try
{
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
string errorMessage = await response.Content.ReadAsStringAsync()
_logger.LogError(ex, errorMessage);
throw;
}
return await response.Content.ReadFromJsonAsync<User>();
}
Another thought that came would be maybe it's possible to add the message to the exception itself and see it when it's thrown? Would it make sense to add the message as an inner exception?
Is there a widely used way to handle the response that comes back after an error is produced in the API? I want to know why a 400 error occurred so I know what to fix. I just don't know what is the "right" way to handle these response messages.
Generally, exception details are only logged, and not returned. This is because details may include personally identifiable information or technical details that could reveal potential security vulnerabilities. There is an error details RFC that is becoming more common, but even that should not have details like PII or a stack trace.
In the case of one API (the MVC endpoint) calling another API (the actual API), the MVC endpoint should return a code in the 5xx range. Either 500 or 502 would be acceptable here. All such errors should be logged on the server side along with their details.
Note that the default behavior is to return 500 if an exception is propagated, so keeping the throw; is all you really need to do. However, it's normal to do error logging in the "pipeline", e.g., middleware for ASP.NET Core or something like a globally-installed action filter for ASP.NET MVC. This is to ensure all errors are logged while avoiding repetition.
EnsureSuccessStatusCode throws an HttpRequestException if the StatusCode is different than 2xx.
In order to gain the most information from the response, you have to retrieve it manually.
The general flow could be described in the following way:
Issue the request inside a try-catch block.
If there was no exception then examine the response's statusCode.
If it is different than the expected one(s) then try to read the response's body
And log everything.
Step #1
HttpResponseMessage response = null;
try
{
response = await httpClient.PutAsync(...);
}
catch (InvalidOperationException ioEx)
{
//The request message was already sent by the HttpClient instance, but failed due to some protocol violation
HttpClient.CancelPendingRequests();
//TODO: logging
}
catch (TaskCanceledException tcEX)
{
//The request was not completed due to either it's timed out or cancelled
if(!tcEX.CancellationToken.IsCancellationRequested)
HttpClient.CancelPendingRequests();
//TODO: logging
}
catch (HttpRequestException hrEx)
{
//The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation.
//TODO: logging
}
Step #2
HttpStatusCodes[] validResponseCodes = new [] {
HttpStatusCode.OK,
HttpStatusCode.Created,
HttpStatusCode.NoContent,
};
if(!validResponseCodes.Contains(response?.StatusCode))
{
//Step #3
}
Step #3
string errorResponse = await response.Content.ReadAsStringAsync();
//Try to parse it if you know the structure of the returned json/xml/whatever
My Post method
[HttpPost]
public async Task<IActionResult> Register()
{
User user = new User { Email = "asfa#gmail.com", UserName = "asfa#gmail.com" };
var result = await _userManager.CreateAsync(user, "SS22!faasd");
if (result.Succeeded)
{
// install cookie
await _signInManager.SignInAsync(user, false);
}
else
{
foreach (var error in result.Errors)
{
//how can i see my errors here
}
}
return Ok();
}
I am trying to create user with UserManager and CreateAsync method, but it doesn't work. In postman I always get 200 OK.
UserManager is connected to my database, because I can get my users with get method.
Also I dont have any cshtml files, so I cant output errors on page.
In postman I always get 200 OK.
You are always getting 200 OK because at the end of a method you are returning OK() method.
In order to have some other status messages consider checking if the user is created and only then return OK() method otherwise either throw an exception, like this:
throw new ArgumentException("Wasn't able to add user")
or return some HTTPError, like this:
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Not able to create user");
Note: you can also throw an exception if the user already exists when you try to add a new user.
I am trying to create user with UserManager and CreateAsync method, but it doesn't work.
This is connected with your _userManager.CreateAsync methods logic. Please revisit it.
To find error, I return OK(result), and see this error in postman.
I'm trying to integrate a Microsoft account login into my ASP.NET MVC app, and I have this controller method:
public void SignIn()
{
// HACK - we will be signed into only one account if we are not signed in to MS
if (Request.GetOwinContext().Authentication.User.Identities.Count() <= 1)
{
// Signal OWIN to send an authorization request to Azure
Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "http://localhost:31503/MicrosoftCalendar" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
What I expect to happen is that I'm prompted to log in to my Microsoft account; instead, what happens is this method runs over and over and over again, doing nothing at all, until I get a "too many redirects" error in my browser. How can I get the Challenge method to actually do something?
I have a class OwinStartup in my web project; I have it set to be the OWIN startup class like so:
[assembly: OwinStartup(typeof(Root.OwinStartup))]
However for some reason my breakpoints inside this startup class never get hit; OWIN is never being initialized... actually, wait a second, it is being initialized, but the event handlers for things like OnAuthorizationCodeReceivedAsync are never being hit...
If I step through the code, after Challenge is called in the SignIn method, I get redirected for some reason to a UserController, which in turn redirects me back to the SignIn method. I wonder why I'm winding up in the UserController?
edit: I need more code? all right, this method in Global.asax.cs executes immediately after the OWIN calls:
protected void MvcApplication_BeginRequest(object sender, EventArgs e)
{
#region Set the context GUID cookie
if (null == Request.Cookies[CookieName.ContextGUID])
{
Response.SetCookie(new System.Web.HttpCookie(CookieName.ContextGUID, Guid.NewGuid().ToString()));
}
#endregion
// check to see whether SSL is required
if (System.Web.Security.FormsAuthentication.RequireSSL)
{
// check where the request is originating from
if (Request.UserHostName != "127.0.0.1" && Request.UserHostName != "localhost")
{
// check if the request is secure
if (!Request.IsSecureConnection)
{
string url = null;
// check for querystring segments
if (!String.IsNullOrEmpty(Request.ServerVariables["QUERY_STRING"]))
{
url = String.Format("https://{0}{1}?{2}",
Request.ServerVariables["SERVER_NAME"],
Request.ServerVariables["SCRIPT_NAME"],
Request.ServerVariables["QUERY_STRING"]);
}
else
{
url = String.Format("https://{0}{1}", Request.ServerVariables["SERVER_NAME"], Request.ServerVariables["SCRIPT_NAME"]);
}
// redirect to the secure url
Response.Redirect(url);
}
}
}
// verify the request
if (null != Request)
{
// NOTE: This is a workaround for the following exception thrown by the ReportViewer control when
// using a non-IE browser:
// Missing URL parameter: IterationId
// See the following reference: https://connect.microsoft.com/VisualStudio/feedback/details/556989/?wa=wsignin1.0
if (Request.Path.EndsWith("Reserved.ReportViewerWebControl.axd") &&
Request.QueryString["ResourceStreamID"] != null &&
Request.QueryString["ResourceStreamID"].ToLower().Contains("blank.gif"))
{
// intercept the request and send to actual valid image path
Response.Redirect(Constant.ImageRoot + "blank.gif");
}
}
}
Not sure if this is what's causing the infinite redirect loop but here it is...
This is maybe a shot in the dark, but it looks like the controller isn't returning anything because it is a void method, try adding a return type, I'm not overly familier with OWIN so you'll have to forgive me there but here is an example of what I'm talking about:
public ActionResult SignIn()
{
// Signal OWIN to send an authorization request to Azure
return Request.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "http://localhost:31503/MicrosoftCalendar" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
two small changes which are in the method signature, returning ActionResult and not a void, you may have to do a bit of research here on the class that OWIN actually returns. and second adding the return keyword, note this will not work with the if statement that you have said is a "hack" because you would require two return statements in that scenario
Hope this helps.
Up to now if a web api 2 error happened and I caught it, I'd return a custom object and fill in the error message from the catch. This would however make the actually http.post() go into success method instead of the error and then I'd have to look at my own boolean success variable and if true all good, if false show error. This is kind of annoying as I have to look for errors in 2 different places for 2 different reasons. From Web API 2 is there a way I can make the http.post() trigger the error callback while I fill out the error message if I catch an error in the web api controller?
[HttpPost]
public MyResponseObject UpdateData(RequestObject req)
{
MyResponseObject resp = new MyResponseObject();
resp.Success = true;
try{
// error happens here
}catch(Exception ex){
resp.Success = false;
resp.Msg = ex.Message;
}
return resp;
}
The http.post() call will still be successful but now I have to look in the success callback for my resp.Success to see if it was REALLY successful or not. Sure the API call was able to be made, but something went wrong inside of it. I'd like to just be able to display that message and fail the call so the http.post() error callback is called with the exception message.
Just throw an exception:
throw new HttpResponseException(HttpStatusCode.InternalServerError);
If you want to customize the response that is returned you can create a HttpResponseMessage with more detail:
var response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("We messed up"),
ReasonPhrase = "Error"
}
throw new HttpResponseException(resp);
Documentation here
On my website, there is a registration form. After having filled this in, the user gets redirected to Azure ACS in order to log in. After having logged in, the user gets redirected back to my website and is to be registered and logged in.
The registration form is submitted by a JavaScript. The information that the user has filled in is saved to a cookie by the RedirectToProvider method in the RegisterController and the user is redirected to ACS. When the user has been redirected back to the website from ACS, the cookie is then read by the RegisterUser method in the RegisterController. The problem is: this works 95% of the time. 5% of the time, the cookie is null when the user comes back. I have been unable to track the cause of this and am wondering if there are any known issues or something that I may have overseen. The form code looks like this:
#using (Html.BeginForm("RedirectToProvider", "Register", FormMethod.Post, new { id = "registerForm" }))
... various fields...
<input type="button" class="btn" id="registerSubmitButton" value="Register" onclick="RegisterRedirect()" />
}
The RegisterRedirect() JavaScript that submits the form (with irrelevant functionality left out here):
var RegisterRedirect = function () {
$("#registerForm").valid();
$("#registerForm").submit();
}
The RedirectToProvider method in the RegisterController:
[AllowAnonymous]
[HttpPost]
public ActionResult RedirectToProvider(RegisterViewModel viewModel)
{
if (ModelState.IsValid)
{
var providerUrl = viewModel.SelectedProviderUrl;
viewModel.SelectedProviderUrl = "";
var json = JsonConvert.SerializeObject(viewModel);
try
{
var cookie = new HttpCookie("RegisterViewModel", json)
{
Expires = DateTime.Now.AddMinutes(10)
};
ControllerContext.HttpContext.Response.Cookies.Add(cookie);
}
catch (FormatException)
{
return RedirectToAction("Index", "Error", new { reason = "Cookie saving error." });
}
return Redirect(providerUrl);
}
return RedirectToAction("Index", "Error", new { reason = "Invalid data. Try again." });
}
The user is redirected to ACS and chooses to log in with, for example, Gmail. ACS calls back to my ClaimsAuthenticationManager (configured in web.config). Afterwards, the method to be called back to (configured in ACS) is called and in turn calls the RegisterUser method that is supposed to read the cookie:
[Authorize]
public ActionResult RegisterUser(User user){
var cookie = ControllerContext.HttpContext.Request.Cookies["RegisterViewModel"];
if (cookie != null){
... registers the user...
}
}
95% of the time, the cookie is not null. 5% of the time, something fails and the cookie is null. The fail rate is higher during the first builds of the website after the Azure Emulator has just started, and lower later on. I have read that it could have something to do with sessions. Does anyone see an obvious error or have any advice? Thanks in advance for any help!
I think that the problem is due to the fact that you sometimes get redirected to a different web role instance where the cookie you created is missing.