HackedByChinese HackedByChinese - 1 year ago 176
ASP.NET (C#) Question

How to intercept 404 using Owin middleware


First let me explain the background. I am working on a project that attempts to marry a backend server that uses Web API configured via OWIN- hosted on IIS now, but potentially other OWIN-supported hosts in the future- to a frontend using AngularJS.

The AngularJS frontend is entirely static content. I completely avoid server-side technologies such as MVC/Razor, WebForms, Bundles, anything that has to do with the frontend and the assets it uses, and defer instead to the latest and greatest techniques using Node.js, Grunt/Gulp, etc. to handle CSS compilation, bundling, minification, etc. For reasons I won't go into here, I keep the frontend and server projects in separate locations within the same project (rather than stick them all in the Host project directly (see crude diagram below).


So as far as the frontend is concerned, all I need to do is get my Host to serve my static content. In Express.js, this is trivial. With OWIN, I was able to do this easily using Microsoft.Owin.StaticFiles middleware, and it works great (it's very slick).

Here is my

string dir = AppDomain.CurrentDomain.RelativeSearchPath; // get executing path
string contentPath = Path.GetFullPath(Path.Combine(dir, @"../../../frontend/MyProjectApp")); // resolve nearby frontend project directory

app.UseFileServer(new FileServerOptions
EnableDefaultFiles = true,
FileSystem = new PhysicalFileSystem(contentPath),
RequestPath = new PathString(string.Empty) // starts at the root of the host

// ensure the above occur before map handler to prevent native static content handler

The Catch

Basically, it just hosts everything in
as if it were right inside the root of MyProject.Host. So naturally, if you request a file that doesn't exist, IIS generates a 404 error.

Now, because this is an AngularJS app, and it supports
, I will have some routes that aren't physical files on the server, but are handled as routes in the AngularJS app. If a user were to drop onto an AngularJS (anything other than
or a file that physically exists, in this example), I would get a 404 even though that route might be valid in the AngularJS app. Therefore, I need my OWIN middleware to return the
file in the event a requested file does not exist, and let my AngularJS app figure out if it really is a 404.

If you're familiar with SPAs and AngularJS, this is a normal and straight-forward approach. If I were using MVC or ASP.NET routing, I could just set the default route to an MVC controller that returns my
, or something along those lines. However, I've already stated I'm not using MVC and I'm trying to keep this as simple and lightweight as possible.

This user had a similar dilemma and solved it with IIS rewriting. In my case, it doesn't work because a) my content doesn't physically exist where the rewrite URL module can find it, so it always returns
and b) I want something that doesn't rely on IIS, but is handled within OWIN middleware so it can be used flexibly.

TL;DNR me, for crying out loud.

Simple, how can I intercept a 404 Not Found and return the content of (note: not redirect) my
using OWIN middleware?

Answer Source

I wrote this little middleware component, but I don't know if it's overkill, inefficient, or if there are other pitfalls. Basically it just takes in the same FileServerOptions the FileServerMiddleware uses, the most important part being the FileSystem we're using. It is placed before the aforementioned middleware and does a quick check to see if the requested path exists. If not, the request path is rewritten as "index.html", and the normal StaticFileMiddleware will take over from there.

Obviously it could stand to be cleaned up for reuse, including a way to define different default files for different root paths (e.g. anything requested from "/feature1" that is missing should use "/feature1/index.html", likewise with "/feature2" and "/feature2/default.html", etc.).

But for now, it this works for me. This has a dependency on Microsoft.Owin.StaticFiles, obviously.

public class DefaultFileRewriterMiddleware : OwinMiddleware
    private readonly FileServerOptions _options;

    /// <summary>
    /// Instantiates the middleware with an optional pointer to the next component.
    /// </summary>
    /// <param name="next"/>
    /// <param name="options"></param>
    public DefaultFileRewriterMiddleware(OwinMiddleware next, FileServerOptions options) : base(next)
        _options = options;

    #region Overrides of OwinMiddleware

    /// <summary>
    /// Process an individual request.
    /// </summary>
    /// <param name="context"/>
    /// <returns/>
    public override async Task Invoke(IOwinContext context)
        IFileInfo fileInfo;
        PathString subpath;

        if (!TryMatchPath(context, _options.RequestPath, false, out subpath) ||
            !_options.FileSystem.TryGetFileInfo(subpath.Value, out fileInfo))
            context.Request.Path = new PathString(_options.RequestPath + "/index.html");

        await Next.Invoke(context);


    internal static bool PathEndsInSlash(PathString path)
        return path.Value.EndsWith("/", StringComparison.Ordinal);

    internal static bool TryMatchPath(IOwinContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
        var path = context.Request.Path;

        if (forDirectory && !PathEndsInSlash(path))
            path += new PathString("/");

        if (path.StartsWithSegments(matchUrl, out subpath))
            return true;
        return false;