I am developing an API and so far all pages return JSON except for the 404 page, which is the default ASP.Net 404 page. I would like to change that so that it returns JSON on the 404 page too. Something like this:
{"Error":{"Code":1234,"Status":"Invalid Endpoint"}}
I can get a similar effect if I catch 404 errors in the Global.asax.cs file and redirect to an existing route:
// file: Global.asax.cs
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
if (ex is HttpException && ((HttpException)ex).GetHttpCode() == 404)
{
Response.Redirect("/CatchAll");
}
}
// file: HomeController.cs
[Route("CatchAll")]
public ActionResult UnknownAPIURL()
{
return Content("{\"Error\":{\"Code\":1234,\"Status\":\"Invalid Endpoint\"}}", "application/json");
}
But this returns a 302 HTTP code to the new URL, which is not what I want. I want to return a 404 HTTP code with JSON in the body. How can I do this?
Things I have tried, which did not work...
#1 - Overwriting the default error page
I thought maybe I could just overwrite the default error page within the 404 handler. I tried like so:
// file: Global.asax.cs
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
if (ex is HttpException && ((HttpException)ex).GetHttpCode() == 404)
{
Response.Clear();
Response.StatusCode = 404;
Response.TrySkipIisCustomErrors = true;
Response.AddHeader("content-type", "application/json");
Response.Write("{\"Error\":{\"Code\":1234,\"Status\":\"Invalid Endpoint\"}}");
}
}
But it just continues to serve up the default ASP error page. By the way, I have not modified my web.config
file at all.
#2 - Using a "catch-all" route
I tried adding a "catch-all" route at the end of the routes table. My entire route config now looks like so:
// file: RouteConfig.cs
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "NotFound",
url: "{*data}",
defaults: new { controller = "Home", action = "CatchAll", data = UrlParameter.Optional }
);
}
But this doesn't work either. If I put a breakpoint inside Application_Error()
I can see that it still just ends up there with a 404 error code. I'm not sure how that is possible, given that the catch all route ought to be matched? But anyway it never reaches the catch-all route.
#3 - Adding a route at runtime
I saw this answer on another SO question where it is possible to call a new route from within the error handler. So I tried that:
// file: Global.asax.cs
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
if (ex is HttpException && ((HttpException)ex).GetHttpCode() == 404)
{
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Home");
routeData.Values.Add("action", "CatchAll");
Server.ClearError();
Response.Clear();
IController homeController = new HomeController();
homeController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
}
}
However it gives the following error:
An exception of type 'System.Web.HttpException' occurred in System.Web.Mvc.dll but was not handled in user code
Additional information: A public action method 'CatchAll' was not found on controller
Myproject.Controllers.HomeController
.
The controller definitely has this method. I am calling the API with POST, however I have tried making the controller specifically for post by adding [HttpPost]
before the method, and I still get the same error.