asp.net 5 404 handling - asp.net

I have read tons of articles on handling 404 errors with the new ASP.NET 5 stuff, but I'm still unsure how to set up the following scenario:
Let Web API controllers return 404 errors, perhaps with a custom message in the response body like "the specified spell id could not be found".
All other 404 errors keep the current url, return a 404 status code, and display the contents of either an MVC error view or a static html file (don't care which).
Allow IIS to handle static files rather than ASP.NET (this condition complicates things because it means we can't put a catch-all route or a catch-all middleware to keep all requests from falling through to IIS).
Here's the example that is not working:
In Startup.cs Configure method:
app.UseErrorHandler("/error");
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseMvc(...);
In web.config:
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="404"/>
<error statusCode="404" path="/error/404" responseMode="ExecuteURL" />
<remove statusCode="500"/>
<error statusCode="500" path="/error" responseMode="ExecuteURL" />
</httpErrors>
And a very basic test controller:
[Route("api/blah")]
public class BlahController : Controller
{
// GET: spell/12345
[HttpGet("[action]/{id}")]
public IActionResult Spell(int id)
{
if (id != 12345)
return new HttpNotFoundObjectResult(new { Message = "not there bro" });
return new ObjectResult(new SpellJson
{
Id = id,
Name = "test"
});
}
}
I am running in IIS Express.
Here's what happens:
1. If I browse to a non-existent url like http://localhost:49858/asdf, I see the html generated by my /error/404 view, great!
If I call /api/blah/12345 from javascript, I get my JSON data, great!
If I call /api/blah/999 from javascript, I get a 404 error but the response body is the html generated by /error/404 rather than the JSON { Message: "not there bro" }, bad!
Which part up there is making my controller that explicitly returns a HttpNotFoundObjectResult instead execute the error page? How do I set things up to allow Web API controller methods to return 404 without going to the error page, but catch all other 404 errors with the error page?

Related

ASP.NET Web API not returning custom error information

I have an ASP.NET Web API 2 action method:
[System.Web.Http.HttpPost]
public HttpResponseMessage Create(HttpRequestMessage req)
{
//...
if (success)
return Request.CreateResponse(HttpStatusCode.Created);
return CreateErrorResponse(HttpStatusCode.BadRequest, "you done bad");
}
Until I did "something", upon error it would return http 400 with the custom error text "you done bad". That's the expected result.
It no longer returns the custom text; it just returns the standard "Bad request".
Have been trying to understand what changed to make this happen.
So I tried:
var response = new { message = "you done bad" };
return Request.CreateResponse(HttpStatusCode.BadRequest, response);
Same result.
I then created a new, clean Web API project and I get the result I expect.
How did I break my project?
The problem is related to the CustomErrors configuration in web.config. From #HaukurHaf's answer at Error messages returned from Web API method are omitted in non-dev environment:
Had the same issue. It's indeed because of the custom errors setting.
In a real world scenario, you would definitely want to use a custom
error page in your application, but in order for custom exception
messages to work in the WebAPI you need to disable the custom errors
page.
How to fix this? Luckily, you can use the <location> element in your
web.config to solve this.
Solution:
<!-- General for the application -->
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="YourCustomErrorPage.aspx"/>
</system.web>
<!-- Override it for paths starting with api (your WebAPI) -->
<location path="api">
<system.web>
<customErrors mode="Off" />
</system.web>
</location>
I use this method in my own app, works well.
While I do see that this works, I don't understand why the CustomErrors section breaks it.
You need to change your method as below
[Route("User/GetUserIDCustomMessage")]
[HttpPost]
public HttpResponseMessage GetUserIDCustomMessage(HttpRequestMessage request, int Id)
{
HttpResponseMessage response = null;
if (Id == 1)
{
response = request.CreateResponse(HttpStatusCode.OK);
}
else {
response = request.CreateResponse(HttpStatusCode.BadRequest , "YOu type bad");
}
return response;
}
When I give 1 , it will give me OK message with 200 status code , otherwise it will give 400/Bad Request , with body message 'YOu type bad'
I think you can probably consider creating a instance of HttpResponseMessage and add your custom error message to its Content property.
HttpResponseMessage msg = new HttpResponseMessage(HttpStatusCode.BadRequest);
msg.Content = new StringContent("you done bad");
return msg;
// or can re-throw it
//throw new HttpResponseException(msg);
Please check the following link for details
https://msdn.microsoft.com/en-us/library/system.net.http.httpresponsemessage(v=vs.118).aspx

HttpContext.Current.Items cleared via responseMode="ExecuteURL"?

I avoid the default ASP.NET approach of redirecting on errors (as many people do). Clean AJAX code and SEO are among the reasons.
However, I'm using the following method to do it, and it seems that I may lose HttpContext.Current.Items in the transfer?
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="401" />
<remove statusCode="403" />
<remove statusCode="404" />
<remove statusCode="500" />
<error statusCode="401" responseMode="ExecuteURL" path="/Account/SignIn" />
<error statusCode="403" responseMode="ExecuteURL" path="/Site/Forbidden" />
<error statusCode="404" responseMode="ExecuteURL" path="/Site/NotFound" />
<error statusCode="500" responseMode="ExecuteURL" path="/Site/Error" />
</httpErrors>
I assumed it just performed a Server.Transfer() under the covers, which I understand preserves Items. (See: Scope of HttpContext.Current.Items and http://weblog.west-wind.com/posts/2010/Jan/20/HttpContextItems-and-ServerTransferExecute )
But I'm also capturing something in Items before the "ExecuteURL", and retrieving/outputting it after the transfer (or whatever it is), and it seems to disappear. I've watched it go into the Items collection, I see the Count raise to 5, and then when the value is retrieved there are only 2 items in the collection.
What is going on?
If you'd like to understand more about what I'm doing and recommend an alternate implementation, I'm open to it. I'm using this to push the ELMAH Error Id into a ViewModel in a way that is free from race conditions. (i.e. a common workaround for this that I'm replacing is to merely display the most recent error.) Here's my code:
Global.asax
protected void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args) {
ElmahSupplement.CurrentId = args.Entry.Id;
}
void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs e) {
if (ElmahSupplement.IsNotFound(e.Exception)) {
ElmahSupplement.LogNotFound((e.Context as HttpContext).Request);
e.Dismiss();
}
}
SiteController.cs
public virtual ActionResult Error() {
Response.StatusCode = 500;
return View(MVC.Site.Views.Error, ElmahSupplement.CurrentId);
}
ElmahSupplement.cs
public class ElmahSupplement {
// TODO: This is a rather fragile way to access this info
private static readonly Guid contextId = new Guid("A41A67AA-8966-4205-B6C1-14128A653F21");
public static string CurrentId {
get {
return
// Elmah 1.2 will fail to log when enumerating form values that raise RequestValidationException (angle brackets)
// https://code.google.com/p/elmah/issues/detail?id=217
// So this id could technically be empty here
(HttpContext.Current.Items[contextId] as string);
}
set {
HttpContext.Current.Items[contextId] = value;
}
}
public static void LogNotFound(HttpRequest request) {
var context = RepositoryProxy.Context;
context.NotFoundErrors.Add(new NotFoundError {
RecordedOn = DateTime.UtcNow,
Url = request.Url.ToString(),
ClientAddress = request.UserHostAddress,
Referrer = request.UrlReferrer == null ? "" : request.UrlReferrer.ToString()
});
context.SaveChanges();
}
public static bool IsNotFound(Exception e) {
HttpException he = e as HttpException;
return he != null && he.GetHttpCode() == 404;
}
}
As explained here, the ExecuteURL generates two requests: the first one throws the exception and the second one generates the error response.
Since Context.Items is cleared between requests, your code always see the 2nd request generated hence the diff between the Items.
Try the sugestion in the post: use system.web > customErrors with redirectMode="ResponseRewrite" instead.
I've followed a trace and determined the following. Some is loosely inferred.
The CustomErrorModule (in the IIS module stack) receives the SEND_RESPONSE notification.
The HttpStatus is 500, so it clones the context, sets a new URL (according the the matching custom error rule), and executes the request on this context (see ExecuteRequest).
The purpose of HttpContext.Items per documentation is:
Gets a key/value collection that can be used to organize and share
data between an IHttpModule interface and an IHttpHandler interface
during an HTTP request.
Viewing this function definition critically, of course, there is only "HTTP request". However, it seems likely that the Items dictionary is itself an item in a dictionary keyed on the HttpContext, which is a unique (cloned) reference in this executing child request. The trace shows the full pipeline (all modules, e.g. duplicate authentication) being run for this ExecuteURL, so this isolated context is of course required.
From unmanaged code, it is trivial to GetParentContext. However, from managed code this hierarchy is not available. So, I'm left without a way to retrieve the original Items.
As an alternate solution, it might be functional to leverage a Global.asax variable, since my tests showed the child request sharing an ApplicationInstance, but I'm not certain client access to this is necessarily sequential.
Another, possibly better approach, would be to avoid re-running the entire pipeline; to never exit the MVC handler (e.g. Controller.OnException and TransferToAction). However, this prevents implementing a Single-Point-of-Truth for error page configuration, since errors can also be raised outside of MVC's awareness.

ASP.NET MVC4 error handling for 404, 401 and other exceptions

I'm struggling to understand how to correctly handle errors in ASP.NET MVC4. As an example, I've created a new MVC4 project using the "Internet Application" template and updated my home controller to test out some error cases:
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Hello";
return View();
}
public ActionResult About()
{
throw new HttpException(401, "Not Authorized");
}
public ActionResult Contact()
{
throw new Exception("Oh no, some error occurred...");
}
}
I have enabled customErrors in my web.config file:
<customErrors mode="On"></customErrors>
When I run the app and click "Contact", I see the ~/Views/Shared/Error.cshtml view as expected, since I have the HandleErrorAttribute registered as a global filter.
However, when I click "About", I get the standard ASP.NET yellow error page that says "Runtime Error". Why are these two exceptions being handled differently and how can I get instances of HttpException to get caught using the HandleError attribute?
CustomErrors config
Ideally, I'd like custom error pages for the following:
A custom 404 (not found) page that's nice and user friendly
A custom 401 (not authorised) page that informs the user that they do not have access (e.g. thrown after checking permissions for a particular item in the model)
A generic error page that is used in all other cases (in place of the standard yellow ASP.NET page).
I've created a new "Error" controller with views for each of the scenarios above. I have then updated customErrors in web.config like so:
<customErrors mode="On" defaultRedirect="~/Error/Trouble">
<error statusCode="404" redirect="~/Error/NotFound"></error>
<error statusCode="401" redirect="~/Error/NotAuthorized"></error>
</customErrors>
The 404 page works fine, but I don't get the 401 page at all. Instead, I get the ~/Error/Trouble view (the one specified as the defaultRedirect) when I try to access the About action on the Home controller.
Why is my custom 401 redirect page not working?
ASP.NET uses 401's internally to redirect users to the login page. Wherever you were planning to throw a 401 unauthorized, instead throw a 403 forbidden.
If you really need to return a 401 and not a 403, then you can use:
HttpContext.Current.Response.SuppressFormsAuthenticationRedirect = true
I had a similar problem where I could not get 401 errors to go to my page despite the change to the web.config.
For a 401 you will probably be seeing the standard 401 Unauthorised page, even if you have added 401 to the customerrors section in your web.config. I read that when using IIS and Windows Authentication the check happens before ASP.NET even sees the request, hence you see it's own 401.
For my project I edited the Global.asax file to redirect to a route I had created for 401 errors, sending the user to the "Unauthorised to see this" view.
In the Global.asax:
void Application_EndRequest(object sender, System.EventArgs e)
{
// If the user is not authorised to see this page or access this function, send them to the error page.
if (Response.StatusCode == 401)
{
Response.ClearContent();
Response.RedirectToRoute("ErrorHandler", (RouteTable.Routes["ErrorHandler"] as Route).Defaults);
}
}
and in the Route.config:
routes.MapRoute(
"ErrorHandler",
"Error/{action}/{errMsg}",
new { controller = "Error", action = "Unauthorised", errMsg = UrlParameter.Optional }
);
and in the controller:
public ViewResult Unauthorised()
{
//Response.StatusCode = 401; // Do not set this or else you get a redirect loop
return View();
}

Global.cs Error Handling using System.Net.Mail

I am using the Application_Error in the Global class to handle 404s.
Once a 404 is caught I send an email for the purpose of logging it. To do this I call a static method to write the error.
ErrorHandler.WriteError(Exception ex)
Within this method I create a new instance of my EmailHandler in order to initialise the System.Net.Mail class. Within my EmailHandler object I have a private member
private MailMessage m_mail = null;
In the constructor of the EmailHandler class I create a new MailMesage instance in order to construct the email.
public EmailHandler()
{
m_mail = new MailMessage();
}
At this point I get the following error.
System.Security.Cryptography.CryptographicException
"The handle is invalid."
I need to point out that this only happens when the request is to a specific file only (such as a www.mysite.com/random-incorrect-image.jpg).
It doesn't happen when I am requesting a page like www.mysite.com/random-incorrect-page.aspx
Maybe you should try the following for static file 404 :
Configure custom 404 in your web.config
<system.webServer>
<httpErrors>
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" prefixLanguageFilePath="" path="/yourpath/yourcustom404.aspx" responseMode="ExecuteURL" />
</httpErrors>
404 Redirection for static files will then occur to http://www.mysite.com/yourpath/yourcustom404.aspx?404;http://www.mysite.com/random-incorrect-image.jpg
You can then process the queryString to extract the url in the pageLoad of custom404.aspx, send your email, and display a friendly message
You can do the same for aspx pages.
In your web.config :
<customErrors mode="On">
<error statusCode="404" redirect="/yourpath/yourcustom404.aspx" />
</customErrors>
the redirection will be slightly different though :
http://www.mysite.com/yourpath/yourcustom404.aspx?aspxerrorpath=/random-incorrect-page.aspx

How to override web.config customErrors settings at runtime?

I have the following code in the web.config file for my ASP.NET C# app that's hosed on Azure:
<!-- Turn on Custom Errors -->
<!-- Switch the mode to RemoteOnly for Retail/Production -->
<!-- Switch the mode to On for to see error pages in the IDE during development -->
<customErrors mode="On" defaultRedirect="ErrorPage.aspx">
<error statusCode="403" redirect="ErrorPage403.aspx"/>
<error statusCode="404" redirect="ErrorPage404.aspx"/>
</customErrors>
This works great for errors when I'm hitting my site site natively (http://ipredikt.com/ErrorPage.aspx), but I also have a Facebook version of the app in which all of the pages use a different MasterPage and hence a different URL (http://ipredikt.com/ErrorPageFB.aspx).
Is it possible to modify the customError redirect values at runtime when I'm running in Facebook app mode, as if I had the following settings in web.config:
<customErrors mode="On" defaultRedirect="ErrorPageFB.aspx">
<error statusCode="403" redirect="ErrorPage403FB.apx"/>
<error statusCode="404" redirect="ErrorPage404FB.apx"/>
</customErrors>
I don't think I can set this at the Application scope since it's individual pages in my app that have knowledge of whether they are running in Facebook mode.
Hi I think what you can do is make another redirect inside your custom error page acording to the referrer - Request.UrlReferrer sometime the referrer is null so make sure you deal with that
So here's a brute force solution. I'm using this on the page for the non-Facebook mode 404 errors:
protected override void OnInit(System.EventArgs e)
{
// If the user tries, for example, to navigate to" /fb/foo/bar
// then the Request.Url.Query will be as follows after the 404 error: ?aspxerrorpath=/fb/foo/bar
string queryString = Request.RequestContext.HttpContext.Request.Url.Query;
string[] str = queryString.Split('=');
if (str.Length > 0)
{
string[] str2 = str[1].Split('/');
if (str2.Length > 1)
{
string test = str2[1].ToLowerInvariant();
if (test == "fb")
{
string pathAndQuery = Request.RequestContext.HttpContext.Request.Url.PathAndQuery;
string absolutePath = Request.RequestContext.HttpContext.Request.Url.AbsolutePath;
string mungedVirtualPath = pathAndQuery.Replace(absolutePath, "/ErrorPage404FB.aspx");
Response.Redirect(mungedVirtualPath);
}
}
}
base.OnInit(e);
}
Hardly ideal, but it works.
"Facebook mode" seems like something you could track in Session, which would be accessible in ErrorPage.aspx to trigger a transfer to ErrorPageFB.aspx.
Update - you can clean up your brute-force solution quite a bit by using Request.QueryString:
protected override void OnInit(System.EventArgs e)
{
// If the user tries, for example, to navigate to" /fb/foo/bar
// then the Request.Url.Query will be as follows after the 404 error: ?aspxerrorpath=/fb/foo/bar
var requestedPath = Request.RequestContext.HttpContext.Request.QueryString["aspxerrorPath"];
if (requestedPath.StartsWith("/fb/", StringComparison.OrdinalIgnoreCase))
{
var requestedUrl = Request.RequestContext.HttpContext.Request.Url;
var pathAndQuery = requestedUrl.PathAndQuery;
var absolutePath = requestedUrl.AbsolutePath;
var mungedVirtualPath = pathAndQuery.Replace(absolutePath, "/ErrorPage404FB.aspx");
Response.Redirect(mungedVirtualPath);
}
base.OnInit(e);
}
Does Request.RequestContext.HttpContext.Request actually return a different instance than simply Request?
The easy way to do it is using session but if you don't use session on you website you can always use cookie and when the user arrive to the error page examine the cookie and decide if you want to redirect him to a new error page

Resources