Search code examples
vuejs2url-rewritinghtml5-historyazure-webapps

Trouble with URL rewrite using Vue Router HTML5 History Mode on Azure Web App


I serve a Vue SPA from an Azure Web App. There is no server side logic (or none relevant to this problem anyway).

I am using Vue Router in HTML5 history mode. It works really well. The only problem is what happens when a user tries to access a view inside the app with a "direct" URL such as https://my.app.com/contacts. This gives a 404 Not Found, as expected.

The workaround is well known: use URL Rewriting to route such requests to the root of the app: / or /index.html.

So that's what I'm trying to do. For Azure Web Apps, the way to do it is using rewrite rules in a web.config file. My Web.config looks like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Handle History Mode and custom 404/500" stopProcessing="true">
          <match url="(.*)" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

This is lifted straight out of the documentation for Vue Router, here.

When I access the site now, I get a blank page. Using the developer console (in Chrome) and checking the network tab, I discover that all requests now return the content of index.html, including script files and css files.

So the rewriting clearly works, just a little too much.

The problem seems to be the (negated) IsFile and IsDirectory conditions. These use the REQUEST_FILENAME server variable. From this page I understand that this is supposed to be the mapped path on the server file system.

As all requests are rewritten, it must be because the conditions fail to recognize the paths as valid file or directory paths.

Edit: I have used failed request tracing to debug the url rewriting, and have learned some more. It turns out that when the rewrite module runs, the REQUEST_FILENAME variable is incorrect.

In the azure web app, the content root is at D:\site\wwwroot. In this folder is my app's wwwroot folder. Some googling reveals that this is as expected. Also, the mapping works correctly - with no url rewriting enabled, an url path like /img/logo.png will return the static file at D:\site\wwwroot\wwwroot\img\logo.png.

But when the url rewrite module runs, REQUEST_FILENAME will have the value D:\site\wwwroot\img\logo.png. That file does not exist, and so the IsFile condition fails.

The question is then: why does the url rewrite module and the static file provider disagree on the path mapping?


Solution

  • I have finally figured this out.

    Although ASP.NET Core apps use IIS, they no longer let IIS serve static files. Static files are kept in a subdirectory (wwwroot) and are served by the static files middleware, configured in Startup.cs like this

    app.UseDefaultFiles();
    app.UseStaticFiles();
    

    The static files middleware knows where the static files are located (this can be configured otherwise if you like), and will serve requests that point to any file in that directory.

    In Azure Web Apps, for historical reasons, the content root, where your .NET Core assemblies and web.config reside, is at D:\home\site\wwwroot by default, so for a request to:

    http:/myapp.com/img/dang.png
    

    the middleware will check if the file:

    D:\home\site\wwwroot\wwwroot\img\dang.png
    

    exists (yes, that's two wwwroots).

    IIS, however, is unaware of this. So as far as IIS is concerned, the url:

    http:/myapp.com/img/dang.png
    

    resides at:

    D:\home\site\wwwroot\img\dang.png
    

    That is why the {REQUEST_FILENAME} IIS variable misses its mark, and why IsFile and IsDirectory will fail.

    So, to sum up: IIS URL rewriting still works in Azure Web Apps, but any matching depending on url-to-file mapping will fail.

    Happily (I have now discovered), there exists an alternative - the URL Rewriting Middleware. This is an ASP.NET Core component available "out of the box" as part of the Microsoft.AspNetCore.App metapackage. It lets you do all the things IIS URL Rewriting lets you do (and more, as it allows for extensions with arbitrary coded logic), but gets it right with static files. The documentation is here.

    Here is the rewrite rule I created:

    public class Html5HistoryRewriteRule : IRule
    {
        private readonly string _rewriteTo;
        private readonly Regex _ignore;
    
        public Html5HistoryRewriteRule(string rewriteTo = null, string ignore = null)
        {
            _rewriteTo = rewriteTo ?? "/";
            _ignore = ignore != null ? new Regex(ignore) : null;
        }
    
        public void ApplyRule(RewriteContext context)
        {
            var request = context.HttpContext.Request;
            var path = request.Path.Value;
    
            if (string.Equals(path, _rewriteTo, StringComparison.OrdinalIgnoreCase))
                return;
            if (_ignore != null && _ignore.IsMatch(path))
                return;
    
            var fileInfo = context.StaticFileProvider.GetFileInfo(path);
            if (!fileInfo.Exists)
            {
                request.Path = _rewriteTo;
                context.Result = RuleResult.SkipRemainingRules;
            }
        }
    }
    

    Edit: In (a very late) response to Maya's comment, here's how I add the rewrite rule in Startup.cs:

    app.UseHtml5HistoryRewrite(ignore: "^/api");
    app.UseDefaultFiles();
    app.UseStaticFiles();
    

    UseHtml5HistoryRewrite is a convenience method in a static class:

    public static class Html5HistoryRewrite
    {
        public static IApplicationBuilder UseHtml5HistoryRewrite(this IApplicationBuilder app, string rewriteTo = null, string ignore = null)
        {
            var rewriteOptions = new RewriteOptions();
            rewriteOptions.Add(new Html5HistoryRewriteRule(rewriteTo, ignore));
            app.UseRewriter(rewriteOptions);
            return app;
        }
    }
    

    The rewrite rule in web.config is no longer required.