0
votes

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.

2

2 Answers

0
votes

We have a catch-all route at the end of our route tables that looks a bit like

        config.Routes.MapHttpRoute(
            name: "NotImplemented",
            routeTemplate: "{*data}",
            defaults: new { controller = "Error", action = "notimplemented", data = UrlParameter.Optional });

where we generate a custom response. You might be able to do something similar in your route table.

0
votes

I finally found a solution that works. It is basically #1 from above, with an extra line added:

// file: Global.asax.cs
protected void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();
    if (ex is HttpException && ((HttpException)ex).GetHttpCode() == 404)
    {
        Server.ClearError(); // important!!!
        Response.Clear();
        Response.StatusCode = 404;
        Response.TrySkipIisCustomErrors = true;
        Response.AddHeader("content-type", "application/json");
        Response.Write("{\"Error\":{\"Code\":1234,\"Status\":\"Invalid Endpoint\"}}");
    }
}

The Server.ClearError(); command appears to be very important. Without it the regular ASP error page is returned and none of the Response. methods have any effect on the returned data.