MVC Controller Action that doesn't modify existing response - c#

In short, I've written an error handler controller that has a "HandleError" action that handles all http errors that I care to handle.
For a few reasons (outlined in this answer), I'm catching http errors in two places. One is a global Application_EndRequest method (managed pipeline) and the other is via the <httpErrors> section in the web config (native custom errors module).
If you're wondering why I need the <httpErrors> section, it's because certain responses aren't always caught by the managed pipeline, for example the StaticFile handler catches all urls like ".html,.txt" and will not trigger MVC code.
My controller action looks close to this
public ActionResult HandleError(int statusCode = 0, Exception exception = null)
{
string responseProcessed = Request.Headers.Get("__p__");
if (responseProcessed == null)
{
Request.Headers.Add("__p__", "1");
switch (statusCode)
{
case 401:
return Unauthorized();
case 403:
return Forbidden();
case 404:
return NotFound();
case 500:
return InternalError(exception);
default:
return UnhandledError(exception, statusCode);
}
}
else
{
return null;
}
}
My web.config httpErrors section is currently
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="404"/>
<error statusCode="404" path="/Errors/HandleError?statusCode=404" responseMode="ExecuteURL"/>
</httpErrors>
Problem
The problem is, the HandleError method is called twice per 404 response because it first hits the custom errors module (specified via the web.config) and then my Application_EndRequest hook catches the 404 which also triggers my action.
Question
The question is, how can I make my HandleError action do nothing to the response instead of replacing it with a blank response (which is what happens when you return null in an action).
Note
Please note that I am aware that I can make my custom 404 error (in web.config) point to my NotFound action explicitly and just return the view each time even though it gets called twice. There is logic (logging, etc) that still needs to only run once and I think it's better design to have it all point to the HandleError method instead to keep repetitive checking logic down and to only runs the action once.

The simple answer is... you cant. All action methods return a type of ActionResult, no matter what. If you do not need an actionresult (or the default EmptyResult that gets sent when you return null) then don't use an action method and consider using a private method instead.
If you do this, you could do as you suggest with a custom 404 and then call this private HandleError method as needed from within your controller.
If this logic needs ran for every request then consider just put it in an action filter instead.

I think you can but you might be over complicating stuff. Lets look at that first :) Does this solve your error handling of static files?
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
You can read more about it here: http://www.hanselman.com/blog/BackToBasicsDynamicImageGenerationASPNETControllersRoutingIHttpHandlersAndRunAllManagedModulesForAllRequests.aspx

Related

Where EXACTLY Do I Catch 404 And 500 Errors?

I know of a couple ways to catch 404 and 500 errors. For example, I can use web.config, route.config, global.asax, and a controller; however, I don't where to put the code to make it catch these errors. Let me tell you what I've done.
Route Config:
The route.config works for 404 errors, but it won't work for 500 errors (to my knowledge). Regardless, I DON'T want to use it because I've heard that it has some downsides to it. Also, it seems to be a very poor solution to this problem IMO.
web/Web Config:
I have tried every possible web config file I have: web.config (system.web and system.webServer) and Web.config (system.web and system.webServer) as well as the Web Debug File (system.web and system.webServer). ALL of these didn't catch the error (It frustrates me because no one will tell me where EXACTLY to put the code so it catches. i.e., place1 -> place2 -> place3 -> etc... Every answer gives me the code and either says Web.config or web.config - I know that but WHERE in Web.config or web.config?) I heard this way has restrictions, but they aren't relevant to me. I know it only works for IIS 7+. I think my team and I are using IIS 8, so that shouldn't be a problem for me. I prefer a method in global.asax or web.config/Web.config.
Global Asax:
I am using the two application error handler methods Application_EndRequest and Application_Error, both of which aren't working. The only error I get is a 200 error (this error is only caught by EndRequest), which isn't an error but quite the opposite. I don't know why I would be getting this when my page shows a 404 error. To my knowledge, it's because Global Asax isn't programmed to catch these errors, but web.config and/or Web.config, and as you know, I don't know how to make that work. I will accept a global.asax solution because I haven't heard anything bad about it, yet.
Controller:
I only tried one solution: protected override void HandleUnknownAction(string actionName), and the solution called for a route in route.config {*url}. Surprise, surprise, this didn't work either (as expected). I learned that this solution using the code {*url} to find 404 errors (or at least it was the solution I searched.) And as I said earlier, I don't want a route.config solution. If I am correct, protected override void HandleUnknownAction(string actionName) may work in global but not for me.
Tried Solutions:
protected override void HandleUnknownAction(string actionName)
protected void Application_EndRequest()
protected void Application_Error(object sender, EventArgs e)
protected override void OnException(ExceptionContext filterContext)
5.
<system.webServer>
<httpErrors errorMode="Custom" defaultResponseMode="File" >
<remove statusCode="404" />
<remove statusCode="500" />
<error statusCode="404"
path="404.html" />
<error statusCode="500"
path="500.html" />
</httpErrors>
</system.webServer>
6.
<httpErrors>
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" prefixLanguageFilePath=""
path="http://yoursite.com/index.asp?syserror" responseMode="Redirect" />
</httpErrors>
7.
<customErrors mode="On">
<error statusCode="404" redirect="/Custom404.html" />
<error statusCode="500" redirect="/Custom500.html" />
</customErrors>
8.
routes.MapRoute(
"Error", // Route name
"Error/{errorCode}", // URL with parameters
new { controller = "Page", action = "Error", errorCode= UrlParameter.Optional }
);
9.
if (Context.Response.StatusCode == 404)
{
Response.Clear();
var rd = new RouteData();
rd.DataTokens["area"] = "AreaName"; // In case controller is in another area
rd.Values["controller"] = "Errors";
rd.Values["action"] = "NotFound";
IController c = new ErrorsController();
c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
}
10.
Exception exception = Server.GetLastError();
// Log the exception.
ILogger logger = Container.Resolve<ILogger>();
logger.Error(exception);
Response.Clear();
HttpException httpException = exception as HttpException;
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
if (httpException == null)
{
routeData.Values.Add("action", "Index");
}
else //It's an Http Exception, Let's handle it.
{
switch (httpException.GetHttpCode())
{
case 404:
// Page not found.
routeData.Values.Add("action", "HttpError404");
break;
case 500:
// Server error.
routeData.Values.Add("action", "HttpError500");
break;
// Here you can handle Views to other error codes.
// I choose a General error template
default:
routeData.Values.Add("action", "General");
break;
}
}
Note: Firstly, I may have adjusted some of this solutions to fit my code, Secondly, I take no credit for this solutions. I found most, if not all, of the solutions on other forum pages. Thirdly, the only solution that worked is the 8th one; however, I believe it only works for 404s. Nevertheless, I don't want to use it because I believe it is a bad solution (correct me if I'm wrong.)
Conclusion:
I am NOT asking you to solve the solution for me. I simply need two thing: one, I need to be corrected if I was misinformed (through a comment or answer); and two, I need to know WHERE to put the code and the result of the code (through either a picture or an explanation.) If you put something like the following:
Here is code that worked for me
[Insert Code Here]
[Insert more description]
I will most likely copy the code, change it, try it, and inevitably get upset if/when it fails. If you could take the time to explain how 404 errors are caught and a global.asax or web/Web Config Solution, I will greatly appreciate it. I have been struggling with this problem for a while now, and I have put in a lot of time and effort into it, only to get vague solutions with little to no explanation as to why it catches 404/500 errors or where to put it, exactly.
Edit:
Here are the error methods I want to hit. I can assure you that they are routed in my route.config using routes.MapMvcAttributeRoutes();
[Route("~/page_not_found")]
public ActionResult PageNotFound()
{
Response.StatusCode = 404;
return View();
}
[Route("~/internal_server_error")]
public ActionResult InternalServerError()
{
Response.StatusCode = 500;
return View();
}
my experience was in this direction.
In terms of user experience, 500 redirects are made so that he does not see the wrong page. but redirecting 500 does not send exception parameters to the redirected page.
To log the error with 500, it is necessary to use global.asax.

405 method not allowed Web API

This error is very common, and I tried all of the solutions and non of them worked. I have disabled WebDAV publishing in control panel and added this to my web config file:
<handlers>
<remove name="WebDAV"/>
</handlers>
<modules runAllManagedModulesForAllRequests="true">
<remove name="WebDAVModule"/>
</modules>
The error still persists. This is the controller:
static readonly IProductRepository repository = new ProductRepository();
public Product Put(Product p)
{
return repository.Add(p);
}
Method implementation:
public Product Add(Product item)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
item.Id = _nextId++;
products.Add(item);
return item;
}
And this is where the exception is thrown:
client.BaseAddress = new Uri("http://localhost:5106/");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.PostAsJsonAsync("api/products", product);//405 exception
Any suggestions?
You are POSTing from the client:
await client.PostAsJsonAsync("api/products", product);
not PUTing.
Your Web API method accepts only PUT requests.
So:
await client.PutAsJsonAsync("api/products", product);
I had the same exception. My problem was that I had used:
using System.Web.Mvc; // Wrong namespace for HttpGet attribute !!!!!!!!!
[HttpGet]
public string Blah()
{
return "blah";
}
SHOULD BE
using System.Web.Http; // Correct namespace for HttpGet attribute !!!!!!!!!
[HttpGet]
public string Blah()
{
return "blah";
}
My problem turned out to be Attribute Routing in WebAPI. I created a custom route, and it treated it like a GET instead of WebAPI discovering it was a POST
[Route("")]
[HttpPost] //I added this attribute explicitly, and it worked
public void Post(ProductModel data)
{
...
}
I knew it had to be something silly (that consumes your entire day)
I tried many thing to get DELETE method work (I was getting 405 method not allowed web api) , and finally I added [Route("api/scan/{id}")] to my controller and was work fine.
hope this post help some one.
// DELETE api/Scan/5
[Route("api/scan/{id}")]
[ResponseType(typeof(Scan))]
public IHttpActionResult DeleteScan(int id)
{
Scan scan = db.Scans.Find(id);
if (scan == null)
{
return NotFound();
}
db.Scans.Remove(scan);
db.SaveChanges();
return Ok(scan);
}
This error can also occur when you try to connect to http while the server is on https.
It was a bit confusing because my get-requests were OK, the problem was only present with post-requests.
Chrome often times tries to do an OPTIONS call before doing a post. It does this to make sure the CORS headers are in order. It can be problematic if you are not handling the OPTIONS call in your API controller.
public void Options() { }
I'm late to this party but as nothing above was either viable or working in most cases, here is how this was finally resolved for me.
On the server the site/service was hosted on, a feature was required!
HTTP ACTIVATION!!!
Server Manager > Manage > Add Roles and Features > next next next till you get to Features > Under .NET (each version) tick HTTP Activation.
Also note there is one hidden under >net > WCF Services.
This then worked instantly!
That was melting my brain
I was getting the 405 on my GET call, and the problem turned out that I named the parameter in the GET server-side method Get(int formId), and I needed to change the route, or rename it Get(int id).
You can also get the 405 error if say your method is expecting a parameter and you are not passing it.
This does NOT work ( 405 error)
HTML View/Javascript
$.ajax({
url: '/api/News',
//.....
Web Api:
public HttpResponseMessage GetNews(int id)
Thus if the method signature is like the above then you must do:
HTML View/Javascript
$.ajax({
url: '/api/News/5',
//.....
If you have a route like
[Route("nuclearreactors/{reactorId}")]
You need to use the exact same parameter name in the method e.g.
public ReactorModel GetReactor(reactorId)
{
...
}
If you do not pass the exact same parameter you may get the error "405 method not allowed" because the route will not match the request and WebApi will hit a different controller method with different allowed HTTP method.
This does not answer your specific question, but when I had the same problem I ended up here and I figured that more people might do the same.
The problem I had was that I had indeliberately declared my Get method as static. I missed this an entire forenoon, and it caused no warnings from attributes or similar.
Incorrect:
public class EchoController : ApiController
{
public static string Get()
{
return string.Empty;
}
}
Correct:
public class EchoController : ApiController
{
public string Get()
{
return string.Empty;
}
}
Here is one solution:
<handlers accessPolicy="Read, Script">
<remove name="WebDAV" />
</handlers>
learn.microsoft.com solution article
and remove WebDAV from modules
<remove name="WebDAVModule" />
[HttpPost] is unnecessary!
[Route("")]
public void Post(ProductModel data)
{
...
}
I could NOT solve this. I had CORS enabled and working as long as the POST returned void (ASP.NET 4.0 - WEBAPI 1). When I tried to return a HttpResponseMessage, I started getting the HTTP 405 response.
Based on Llad's response above, I took a look at my own references.
I had the attribute [System.Web.Mvc.HttpPost] listed above my POST method.
I changed this to use:
[System.Web.Http.HttpPostAttribute]
[HttpOptions]
public HttpResponseMessage Post(object json)
{
...
return new HttpResponseMessage { StatusCode = HttpStatusCode.OK };
}
This fixed my woes. I hope this helps someone else.
For the sake of completeness, I had the following in my web.config:
<httpProtocol>
<customHeaders>
<clear />
<add name="Access-Control-Expose-Headers " value="WWW-Authenticate"/>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, OPTIONS, PUT, PATCH, DELETE" />
<add name="Access-Control-Allow-Headers" value="accept, authorization, Content-Type" />
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
Old question but none of the answers worked for me.
This article solved my problem by adding the following lines to web.config:
<system.webServer>
<modules runAllManagedModulesForAllRequests="false">
<remove name="WebDAVModule" />
</modules>
</system.webServer>
In my case I had a physical folder in the project with the same name as the WebAPI route (ex. sandbox) and only the POST request was intercepted by the static files handler in IIS (obviously).
Getting a misleading 405 error instead of the more expected 404, was the reason it took me long to troubleshoot.
Not easy to fall-into this, but possible. Hope it helps someone.
Make sure your controller inherits from Controller class.
It might even be crazier that stuff would work locally even without that.
For my part my POST handler was of this form:
[HttpPost("{routeParam}")]
public async Task<ActionResult> PostActuality ([FromRoute] int routeParam, [FromBody] PostData data)
I figured out that I had to swap the arguments, that is to say the body data first then the route parameter, as this:
[HttpPost("{routeParam}")]
public async Task<ActionResult> PostActuality ([FromBody] PostData data, [FromRoute] int routeParam)
check in your project .csproj file and change
<IISUrl>http://localhost:PORT/</IISUrl>
to your website url like this
<IISUrl>http://example.com:applicationName/</IISUrl>
Another possible issue which causes the same behavior is the default parameters in the routing. In my case the controller was located and instantiated correctly, but the POST was blocked because of default Get action specified:
config.Routes.MapHttpRoute(
name: "GetAllRoute",
routeTemplate: "api/{controller}.{ext}"/*,
defaults: new { action = "Get" }*/ // this was causing the issue
);
I was having exactly the same problem. I looked for two hours what was wrong with no luck until I realize my POST method was private instead of public .
Funny now seeing that error message is kind of generic. Hope it helps!
We had a similar issue. We were trying to GET from:
[RoutePrefix("api/car")]
public class CarController: ApiController{
[HTTPGet]
[Route("")]
public virtual async Task<ActionResult> GetAll(){
}
}
So we would .GET("/api/car") and this would throw a 405 error.
The Fix:
The CarController.cs file was in the directory /api/car so when we were requesting this api endpoint, IIS would send back an error because it looked like we were trying to access a virtual directory that we were not allowed to.
Option 1: change / rename the directory the controller is in
Option 2: change the route prefix to something that doesn't match the virtual directory.
In my case, the 405 error only showed up in production server, and not on my dev machine.
I found that the problem was due to the fact that I simply "manually" transferred the contents of the locally published folder from my local machine to the online production server.
So, the FIX for me was to simply delete all the online files on the prod server, and then use the "Publish" option on Visual Studio to publish directly from my local machine to the prod server via FTP.
I don't know exactly why this changed something, because it seems to me the files were the same, but this thing fixed the problem and I hope it could help someone else too.
Another possible cause can be to do with Session State config in IIS causing a redirect which appends "?AspxAutoDetectCookieSupport=1" to the URL. In my case I was performing a POST but the redirect was being performed as a GET by the HttpClient.
The solution I found was to add the following to my web.config:
<system.web>
<sessionState cookieless="UseCookies" />
</system.web>
Function names make it complicated for c# sometimes. Change name of the function, it will works. Like ProductPut instead of PutProduct or Put.
public Product ProductPut(Product p)
{
return repository.Add(p);
}

Correctly send user to 404 if dynamic content is not found (ASP.NET MVC)

I have implemented 404 handling for the general case in ASP.NET MVC 3, for when a controller/view is not found. But how should it be handled inside the controller if the user is trying to access something that can't be found? For example www.foo.bar/Games/Details/randomjunk will call this inside GamesController:
public ActionResult Details(string id) // id is 'randomjunk'
{
if(DoesGameExist(id) == false)
// Now what?
I could just do a return Redirect('/Errors/Http404'); but that doesn't seem like the correct way to do it. Should you throw an exception, or something else?
We could have a special view in this case, but to start with we need a good way we can apply to several cases.
Edit: I want to show my friendly 404 page I already have for the general case.
You should throw HttpException 404 :
throw new HttpException(404, "Page not Found");
EDIT:
Apparently per Darin Dimitrov, what I had before doesn't work even with customErrors. As Antonio Bakula says in the other answer, you have to do:
throw new HttpException(404, "Not found")
Then the customErrors will work.
There's a built-in helper method called HttpNotFound so you can just do:
return HttpNotFound();
You could also explicitly return a 404 with HttpStatusCodeResult:
return new HttpStatusCodeResult(404);
HttpStatusCodeResult is helpful when there's not a specific helper method or class for the code you want.
You should also have this in your Web.config:
<customErrors>
<error statusCode="404" redirect="~/Custom404Page.html"/>
</customErrors>
You can also have additional lines for other statuses, and a defaultRedirect.
You could return a HttpStatusCodeResult.
return new httpStatusCodeResult(404);

Custom error pages in different areas in ASP.NET MVC3

I have set up custom error pages on my site using
<customErrors mode="RemoteOnly" defaultRedirect="~/Error">
<error statusCode="500" redirect="~/Error/InternalError"/>
<error statusCode="404" redirect="~/Error/FileNotFound"/>
<error statusCode="403" redirect="~/Error/AccessDenied"/>
</customErrors>
however there is another area on the site, Suppliers, and when an error occurs in the supplier area the redirect goes to Suppliers/Error/_. Since I don't have any error pages here, the site just seems to hang never shows the error pages. How can I fix this without having to copy the error pages to the supplier area?
As far as I understand with MVC your URL make up, by default is:
Domain/Controller/Action/id
If you have an "Error" Controller. In your logic, you test to see if the request originated from an user of the site that would need to redirect to the "Suppliers" Error page
[HandleError]
public ActionResult Index()
{
// Test to see if you need to go to the SuppliersController
if (this.User.IsInRole("supplier"))
{
return Redirect("/Suppliers/Error");
}
else
{
return View(); // This returns the "Error" View from the shared folder
}
}
redirect to an Error handling action on your Suppliers Controller that will return the right view.
public class SuppliersController : Controller
{
//
// GET: /Suppliers/
public ActionResult Error()
{
return View("Error","SomeMasterPage"); // No "Error" view in Suppliers View folder, so it uses the one in shared folder
}
}
You can also use the [Authorize] attribute on your Suppliers Error Action to make sure the user is logged on.
In this way, you will get your desired /Suppliers/Error URL and can use the SuppliersController Action to specify the desired view, model, and master/Layout page.
also look at this very comprehensive reply to a similar question:
Best 404 example I can find for mvc
I guess removing the "~" before the error page should do the trick, you will need the "\" though.
Another way would be to write the FULL URL in the redirect/defaultRedirect attribute.

ASP.NET MVC Controller.OnException not being called

I have a base controller class where I'm overriding to the Controller.OnException handler method in order to provide a generic error handling for certain types of controllers that will inherit from this class (these are API controllers that will return JSON results). The OnException method never gets called when an exception gets raised by the controller. Does anyone see what I am doing wrong, or is there a better way to handle this situation?
Using MVC 1.0
Base Class:
public class APIController : Controller
{
protected override void OnException(ExceptionContext filterContext)
{
//This is never called
filterContext.Result =
new JsonResult();
base.OnException(filterContext);
}
}
Inheriting Class:
public class MyController : APIController
{
public AjaxResult ForcedException()
{
throw new SystemException();
}
}
If I understand your question correctly - your Exception must be marked as "handled" in your OnException. Try this:
filterContext.ExceptionHandled = true;
The MSDN documentation (http://msdn.microsoft.com/en-us/library/system.web.mvc.controller.onexception.aspx) states that the OnException method is "called when an unhandled exception occurs in the action."
So, it sounds like it might only fire when an unhandled exception happens in an action method.
I created an new controller with one Index action. And the OnException method does in fact execute. I also tried throwing a SystemException() and the OnException method fired as well.
public ActionResult Index ( )
{
throw new NotImplementedException ( );
}
I was having exactly the same problem in my ASP.NET MVC 3 project.
It turns out that this is a feature of Visual Studio and the development server. When in debug mode you will be returned to Visual Studio and given the default exception dialog box.
One way to stop this from happening is to change the compilation mode in your Web.config from this:
<compilation debug="true" targetFramework="4.0">
To this:
<compilation debug="false" targetFramework="4.0">
You can also leave debug mode set to true but test your application is working by choosing the Start without debugging option from the visual studio menu (invoked by pressing Ctrl+F5.
Are you calling the controller action from a unit test, or via ASP.NET? If you're calling the method directly in a test, say through NUnit, then OnException won't fire: the test framework will handle the exception and turn it into a failed test.
The same rules apply for Controller.OnException as for HandleErrorAttribute
The required config for HandleErrorAttribute is noted here https://stackoverflow.com/a/528550/939634
If you have customError in your web.config set to mode="RemoteOnly" and you are debugging locally or you have mode="Off" the exceptions will not be handled and you will see the ASP.Net yellow screen with a stack trace.
If you set mode="On" or you are viewing the site remotely and have mode="RemoteOnly" your OnException method will execute properly.
Apply the [HandleError] attribute to the class.

Categories

Resources