title: Custom 404 page with JSON without redirect
body: <p>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:</p>
  
  <pre><code>{"Error":{"Code":1234,"Status":"Invalid Endpoint"}}
  </code></pre>
  
  <p>I can get a similar effect if I catch 404 errors in the Global.asax.cs file and redirect to an existing route:</p>
  
  <pre><code>// file: Global.asax.cs
  protected void Application_Error(object sender, EventArgs e)
  {
      Exception ex = Server.GetLastError();
      if (ex is HttpException &amp;&amp; ((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");
  }
  </code></pre>
  
  <p>But this returns a 302 HTTP code to the new URL, which is not what I want. <strong>I want to return a 404 HTTP code with JSON in the body. How can I do this?</strong></p>
  
  <p><strong>Things I have tried, which did not work...</strong></p>
  
  <p><strong>#1 - Overwriting the default error page</strong></p>
  
  <p>I thought maybe I could just overwrite the default error page within the 404 handler. I tried like so:</p>
  
  <pre><code>// file: Global.asax.cs
  protected void Application_Error(object sender, EventArgs e)
  {
      Exception ex = Server.GetLastError();
      if (ex is HttpException &amp;&amp; ((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\"}}");
      }
  }
  </code></pre>
  
  <p>But it just continues to serve up the default ASP error page. By the way, I have not modified my <code>web.config</code> file at all.</p>
  
  <p><strong>#2 - Using a "catch-all" route</strong></p>
  
  <p>I tried adding a "catch-all" route at the end of the routes table. My entire route config now looks like so:</p>
  
  <pre><code>// 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 }
      );
  }
  </code></pre>
  
  <p>But this doesn't work either. If I put a breakpoint inside <code>Application_Error()</code> 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.</p>
  
  <p><strong>#3 - Adding a route at runtime</strong></p>
  
  <p>I saw <a href="https://stackoverflow.com/a/319090">this answer</a> on another SO question where it is possible to call a new route from within the error handler. So I tried that:</p>
  
  <pre><code>// file: Global.asax.cs
  protected void Application_Error(object sender, EventArgs e)
  {
      Exception ex = Server.GetLastError();
      if (ex is HttpException &amp;&amp; ((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));
      }
  }
  </code></pre>
  
  <p>However it gives the following error:</p>
  
  <blockquote>
    <p>An exception of type 'System.Web.HttpException' occurred in System.Web.Mvc.dll but was not handled in user code</p>
    
    <p>Additional information: A public action method 'CatchAll' was not found on controller <code>Myproject.Controllers.HomeController</code>.</p>
  </blockquote>
  
  <p>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 <code>[HttpPost]</code> before the method, and I still get the same error.</p>