Aspnetcore.docs: Update sample web.config file for Blazor precompressed files

Created on 15 Oct 2020  Β·  29Comments  Β·  Source: dotnet/AspNetCore.Docs

[EDIT by guardrex to add the metadata. @cornem, please use the This page feedback form at the bottom of the English-US topic to open new issues, which will automatically add the metadata]

The docs page for Hosting a Blazor WebAssembly app links to a sample web.config file. However, using this as-is leads to a number of unexpected errors. The problem is that the web.config file correctly resets the MIME types for the file extensions dat, dll, js, json and wasm, but not for other types, even though precompressed files are present for html, css, images, etc. This leads to errors in the browser and those resources are not loaded.

I think the sample web.config could be improved in two ways:

  • Add the proper <mimeMap> records for commonly used files that are being precompressed like html, css, woff, woff2, ico, png, svg, etc.
  • And/or make sure the rewrite rules only apply to configured MIME types

As for the second suggestion, I have updated my own web.config file by adding a condition that checks if the requested file matches the configured MIME types (only showing for br, same is required for gzip):

<rule name="Rewrite brotli file" stopProcessing="true">
  <match url="(.*)"/>
  <conditions>
    <add input="{HTTP_ACCEPT_ENCODING}" pattern="br" />
    <add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm)$" /> <!-- Added -->
    <add input="{REQUEST_FILENAME}.br" matchType="IsFile" />
  </conditions>
  <action type="Rewrite" url="{R:1}.br" />
</rule>

Document Details

⚠ Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

Blazor P1 doc-enhancement

All 29 comments

Hello @cornem ... I'll be able to take a closer look after .NET 5 releases. We're all πŸƒπŸ˜… to get the coverage in place for the release.

A couple of quick points ...

This is a highly customizable scenario that will require tweaks for a given set of compression choices. We weren't seeking to address any further file compressions than the ones that you see. Perhaps, the text should say that the configuration will require modification for the custom specifications of a given app.

"leads to a number of unexpected errors"

You mean due to your assumptions about what the file covers in the base case, right? Aren't your errors just due to you compressing files that the configuration doesn't cover and then removing the uncompressed files from the published app?

commonly used files that are being precompressed like html, css, woff, woff2, ico, png, svg, etc.

WRT images and fonts, the general compression guidance is not to compress already compressed formats, so PNG and font files probably aren't the best candidates. I did notice that we have WOFF/WOFF2 in the current web.config file. It might be best to remove those. There might be anything from little benefit to a detrimental outcome, especially WOFF2 because I think that one has improved compression. WRT HTML/CSS, those are usually small files anyway.

Well it seems that right now publishing a standalone Blazor WebAssembly projects precompresses a whole bunch of files by default. When I publish my project, pretty much every file appears to be precompressed, including images (PNG and SVG), fonts, stylesheets, HTML files. I don't recall specifically asking for the compression of these files, it just happens.

So obviously, based on the rules in the sample file, the Rewrite sections actually finds all these compressed files and uses them instead. But then - I suppose because the MIME type isn't properly mapped - stylesheets and images get rejected by the browser.

E.g. if you request example.com/assets/img0.png, IIS will look for assets/img0.png.br if br is specified as an accepted content encoding. So it returns this precompressed file instead, which leads to an error. To mitigate this, I have added this additional condition to make sure that only the file types specified at the top of the file are actually returned precompressed.

I did notice that we have WOFF/WOFF2 in the current web.config file. It might be best to remove those. There might be anything from little benefit to a detrimental outcome, especially WOFF2 because I think that one has improved compression. WRT HTML/CSS, those are usually small files anyway.

Those are not there for precompression, I think this is simply adding a mime handler absent in IIS by default.

Thanks for explaining. I see what you mean now. Given that this is more serious than I first thought, I'll move it up in priority and get to it soon.

I'll try to reach this early next week, perhaps Monday. Thanks for reporting these problems.

Thanks.

And yeah, I agree it isn't necessary for all of those extensions mentioned. When I look at my own app, I still see savings of 75% on the Brotli/Gzipped HTML and CSS files though (I guess Blazor doesn't minify those by default), not for images as you rightly pointed out.

The original file was built by @benadams for an earlier version of the framework ... in fact ... I added Ben to the author byline for doing the work on setting it up. πŸ₯‡ Thanks again, Ben!

I jokingly refer to devs on Ben's level as :pray: server gods :pray:; but seriously, Ben is the most capable software engineer I've ever known, and my guess is that aspects of compression performed by the framework have evolved since we first put the file up. IIRC, testing at the time went fine. Nothing hit my radar since that time, but that's not really unusual. There are many small changes each release that don't make it into announcements, blog posts, and docs.

I'm actually almost done with the major .NET 5 updates. :+1: I believe that the workload (and pressure 😰) will lighten up here shortly. I hope to work on this on Monday. If it goes straight to a PR, I'll ping you on the PR for a look at the updates and further discussion.

@cornem ~Did you hit a subresource integrity check failure for just the _framework/dotnet.timezones.blat file?~

~Things seem to be going ok here with testing updates to the web.config thus far, but I'm hitting this one little 🐞. I'll dig a bit more on that file and see if I can see what its problem is. Just wondering if you saw something similar in your work with compression.~

btw --- Good reading here :point_right: https://github.com/dotnet/aspnetcore/issues/22999 ... that I might be able to take some doc hints for troubleshooting advice. For example, _HTTPS!_ ... seems like the safe way to perform compression testing, especially in FF. I hit the same stuff that Darrell hit when I tried HTTP in FF.

EDIT: ~What's the MIME type for a .blat file supposed to be? I'll look, but I wonder if application/octet-stream is wrong and thus breaking my config.~

UPDATE: ... mmmmmm ... this looks interesting .... https://github.com/dotnet/aspnetcore/issues/25258

UPDATE: ... oh ... I was right after all ... https://github.com/dotnet/aspnetcore/pull/25284/files

I'm obviously rubber πŸ¦†'ing my way along here. :smile:

UPDATE: Ah ... ok! ... the app is starting to work now. :tada::smile: ... with a strange mix of both gzip and br resources loading!

@cornem ... Let's discuss this _here_. When I put the PR up, anyone who doesn't see this issue might actually think that I have a clue what's going on. (Of course, that would be a wild speculation on someone's part. 😁)

This file is working ...

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <staticContent>
      <remove fileExtension=".dll" />
      <mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".wasm" mimeType="application/wasm" />
      <mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".js.gz" mimeType="application/javascript" />
      <mimeMap fileExtension=".dat.gz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".dll.gz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json.gz" mimeType="application/json" />
      <mimeMap fileExtension=".wasm.gz" mimeType="application/wasm" />
      <mimeMap fileExtension=".blat.gz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".html.gz" mimeType="text/html" />
      <mimeMap fileExtension=".css.gz" mimeType="text/css" />
      <mimeMap fileExtension=".ico.gz" mimeType="image/x-icon" />
      <mimeMap fileExtension=".svg.gz" mimeType="image/svg+xml" />
      <mimeMap fileExtension=".js.br" mimeType="application/javascript" />
      <mimeMap fileExtension=".dat.br" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".dll.br" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json.br" mimeType="application/json" />
      <mimeMap fileExtension=".wasm.br" mimeType="application/wasm" />
      <mimeMap fileExtension=".blat.br" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".html.br" mimeType="text/html" />
      <mimeMap fileExtension=".css.br" mimeType="text/css" />
      <mimeMap fileExtension=".ico.br" mimeType="image/x-icon" />
      <mimeMap fileExtension=".svg.br" mimeType="image/svg+xml" />
    </staticContent>
    <httpCompression>
      <dynamicTypes>
        <remove mimeType="text/*" />
        <remove mimeType="application/javascript" />
        <remove mimeType="image/svg+xml" />
      </dynamicTypes>
      <staticTypes>
        <remove mimeType="text/*" />
        <remove mimeType="application/javascript" />
        <remove mimeType="image/svg+xml" />
      </staticTypes>
    </httpCompression>
    <rewrite>
      <outboundRules rewriteBeforeCache="true">
        <rule name="Add Vary Accept-Encoding" preCondition="PreCompressedFile" enabled="true">
          <match serverVariable="RESPONSE_Vary" pattern=".*" />
          <action type="Rewrite" value="Accept-Encoding" />
        </rule>
        <rule name="Add Encoding Brotli" preCondition="PreCompressedBrotli" enabled="true" stopProcessing="true">
          <match serverVariable="RESPONSE_Content_Encoding" pattern=".*" />
          <action type="Rewrite" value="br" />
        </rule>
        <rule name="Add Encoding Gzip" preCondition="PreCompressedGzip" enabled="true" stopProcessing="true">
          <match serverVariable="RESPONSE_Content_Encoding" pattern=".*" />
          <action type="Rewrite" value="gzip" />
        </rule>
        <preConditions>
          <preCondition name="PreCompressedFile">
            <add input="{HTTP_URL}" pattern="\.(gz|br)$" />
          </preCondition>
            <preCondition name="PreCompressedBrotli">
            <add input="{HTTP_URL}" pattern="\.br$" />
          </preCondition>
          <preCondition name="PreCompressedGzip">
            <add input="{HTTP_URL}" pattern="\.gz$" />
          </preCondition>
        </preConditions>
      </outboundRules>
      <rules>
        <rule name="Serve subdir">
          <match url=".*" />
          <action type="Rewrite" url="wwwroot\{R:0}" />
        </rule>
        <rule name="Rewrite brotli file" stopProcessing="true">
          <match url="(.*)"/>
          <conditions>
            <add input="{HTTP_ACCEPT_ENCODING}" pattern="br" />
            <add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm|blat|htm|html|css|ico|svg)$" />
            <add input="{REQUEST_FILENAME}.br" matchType="IsFile" />
          </conditions>
          <action type="Rewrite" url="{R:1}.br" />
        </rule>
        <rule name="Rewrite gzip file" stopProcessing="true">
          <match url="(.*)"/>
          <conditions>
            <add input="{HTTP_ACCEPT_ENCODING}" pattern="gzip" />
            <add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm|blat|htm|html|css|ico|svg)$" />
            <add input="{REQUEST_FILENAME}.gz" matchType="IsFile" />
          </conditions>
          <action type="Rewrite" url="{R:1}.gz" />
        </rule>
        <rule name="SPA fallback routing" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
          </conditions>
          <action type="Rewrite" url="wwwroot\" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

I dropped your _draft_ version into my production app, and that's working fine.
Then I changed this:

<add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm|blat)$" />

to this:

<add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm|blat|html|css|ico|png|svg|jpg|jpeg|gif)$" />

...which also works fine and happens to save me a few more bytes πŸš€.

As for the precondition tags, I think the reason they exist is so they get picked up by the <outboundRules> section which they're part of, so the correct encoding header is set. So the precondition PreCompressedFile sets the Vary header for both gzip and br, and then the other two are used to set the Content-Encoding header to the correct value.

In my case all the resources loaded _from my own server_ are br encoded. However, I'm also loading some fonts from Google, and those are gzipped.

@guardrex just tried your last post config. I cant still reach js/css/images. However I dont own precompressed js/css/images in gzip/brotli. Just Blazor WASM files. Shouldn't IIS dynamic compression take care of uncompressed js/css/images?

@glararan ... Definitely a work-in-progress at this point. I stopped today as soon as the app started to run at all. I'll pick back up with this soon and continue working it forward. I'll keep that file example :point_up: updated and ping here when there are new developments with it.

@cornem Very good ... I'll perform the same update to match all of the MIMEs that we have here. WRT to the preconditions ... _Ah, yes!_ I didn't spot before that these are _named_ entries for preCondition in the rule entires above. I see that now. Ok. :+1:

@guardrex alright. Thanks!

@guardrex just tried your last post config. I cant still reach js/css/images. However I dont own precompressed js/css/images in gzip/brotli. Just Blazor WASM files. Shouldn't IIS dynamic compression take care of uncompressed js/css/images?

Could you clarify in what way you can't reach those assets? If there are no precompressed files the rules above aren't triggered and then the uncompressed files (if present) are served by IIS. Dynamic compression configured in IIS should then be applied.

@guardrex I did some more testing today. Looks like you can safely remove most of the <remove fileExtension> tags from the top of the file. I think the purpose really is to make sure that file types specific to dotnet-wasm get served properly. I just removed most of the entries and now have only this list, which works fine with and without compression.

Also, I removed the woff and woff2 entries. Brotli was originally conceived exactly for those types of files, so they are 'precompressed' by default. I guess the CLI shouldn't even try to further compress those, but that's probably an issue for another team.
In fact, a funny thing I noticed is that the precompressed web fonts are actually _larger_ than the original 😲

.woff2 - 7,008 bytes
.woff2.br - 7,012 bytes
.woff2.gz - 7,031 bytes

<staticContent>
  <remove fileExtension=".dat" />
  <remove fileExtension=".dll" />
  <remove fileExtension=".json" />
  <remove fileExtension=".wasm" />
  <remove fileExtension=".woff" />
  <remove fileExtension=".woff2" />
  <remove fileExtension=".blat" />

  <mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".json" mimeType="application/json" />
  <mimeMap fileExtension=".wasm" mimeType="application/wasm" />
  <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
  <mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
  <mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".js.gz" mimeType="application/javascript" />
  <mimeMap fileExtension=".dat.gz" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".dll.gz" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".json.gz" mimeType="application/json" />
  <mimeMap fileExtension=".wasm.gz" mimeType="application/wasm" />
  <mimeMap fileExtension=".blat.gz" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".html.gz" mimeType="text/html" />
  <mimeMap fileExtension=".css.gz" mimeType="text/css" />
  <mimeMap fileExtension=".ico.gz" mimeType="image/x-icon" />
  <mimeMap fileExtension=".png.gz" mimeType="image/png" />
  <mimeMap fileExtension=".svg.gz" mimeType="image/svg+xml" />
  <mimeMap fileExtension=".jpg.gz" mimeType="image/jpeg" />
  <mimeMap fileExtension=".jpeg.gz" mimeType="image/jpeg" />
  <mimeMap fileExtension=".gif.gz" mimeType="image/gif" />
  <mimeMap fileExtension=".js.br" mimeType="application/javascript" />
  <mimeMap fileExtension=".dat.br" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".dll.br" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".json.br" mimeType="application/json" />
  <mimeMap fileExtension=".wasm.br" mimeType="application/wasm" />
  <mimeMap fileExtension=".blat.br" mimeType="application/octet-stream" />
  <mimeMap fileExtension=".html.br" mimeType="text/html" />
  <mimeMap fileExtension=".css.br" mimeType="text/css" />
  <mimeMap fileExtension=".ico.br" mimeType="image/x-icon" />
  <mimeMap fileExtension=".png.br" mimeType="image/png" />
  <mimeMap fileExtension=".svg.br" mimeType="image/svg+xml" />
  <mimeMap fileExtension=".jpg.br" mimeType="image/jpeg" />
  <mimeMap fileExtension=".jpeg.br" mimeType="image/jpeg" />
  <mimeMap fileExtension=".gif.br" mimeType="image/gif" />
</staticContent>

@cornem Well, my case is that I have all uncompressed files. However VS generates gzip/brotli for WASM (wwwroot/_framework).

What is strange example config dont use asp hosted model? I mean there is no

  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\app.Server.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
    </system.webServer>
  </location>

Do you have separate web.config? My issue is that IIS returns 404 for non-compressed files.

I believe URL rewrite is correct and should return uncompressed files by design..
image

@glararan Ah, if you're using Hosted, then you don't need this web.config file at all. It's specifically for standalone hosting (static files). If you're using an ASP.NET Core host, that host will take care of serving the right files for you.

@cornem ah that might be the issue. However if I use https://developers.google.com/speed/pagespeed/insights/ then test says I am serving uncompressed dlls. Can you point me right direction how to server compressed gzip/brotli under host? Thanks

@glararan It's on the same page in the docs.

When using an ASP.NET Core hosted project, the host project is capable of performing content negotiation and serving the statically-compressed files.

Unless you've disabled Blazor static compression (on by default), it should work by default according to the docs. You can check yourself what files are served using something like Fiddler or Postman where you can test with different Accept-Encoding request headers, or you can observe the headers sent and received by your browser in the Network tab of the DevTools window. If you find it doesn't work for you, I guess you should create a separate issue either in this repository or https://github.com/dotnet/aspnetcore.

@cornem Oh then I missread that. I did not disable compression during publish. wwwroot/_framework contains compressed files
image

Like you mention network tab. I am actually doing that. And I am getting server uncompressed files.
image

_framework/bin
image

On images you can see mscorlib is server uncompressed.

My app configuration

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
                app.UseWebAssemblyDebugging();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseSession();
            app.UseHttpsRedirection();
            app.UseBlazorFrameworkFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();
            app.UseCookiePolicy();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub(blazorOptions =>
                {
                    blazorOptions.ApplicationMaxBufferSize = blazorOptions.TransportMaxBufferSize = 20 * 1024 * 1024;
                });
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapFallbackToPage("/_Host");
            });
        }

@glararan You should actually look at the Content-Encoding header, notΒ just the filename. The server will always return the file with the same file name as requested. Otherwise you'd need a redirect first. So the filename doesn't say anything about whether the response was compressed or not:

image

@cornem There is none :(

But I might found issue. I turned off CloudFlare proxy. Now I can see content-encoding brotli and file is served brotli!

I guess I have to figure out how to proxy via CloudFlare content-encoding to destination server.

CLI shouldn't even try to further compress those, but that's probably an issue for another team. ... In fact, a funny thing I noticed is that the precompressed web fonts are actually larger than the original

I agree. I've asked the product unit for feedback on that.

safely remove most of the tags from the top of the file

I agree. I don't see any reason to remove and then re-add known good MIME mappings.

I'm still in the midst of updating the file. I'll report the result and update the file shortly.

I've updated to my latest version at https://github.com/dotnet/AspNetCore.Docs/issues/20205#issuecomment-713744364.

Notes on the latest updates:

  • No reason AFAICT to remove and then re-add existing MIME maps. The only one that changes in Ben's file is for DLLs, which go from application/x-msdownload to application/octet-stream.
  • Updated dynamicTypes and staticTypes WRT applicationHost.config: Remove the ones from applicationHost.config that will be compressed by the framework, which are just text/*, JS, and SVG. Everything else is off by default, so AFAICT additional removals aren't needed.
  • For now, I leave the MIME maps and rewrite rules for double-compressed file types. However, I don't really think that that's correct. I'm waiting for confirmation from the product unit on the engineering issue.

Ok ... slept on it :zzz:πŸ›Œ ... I don't think we should mess around with double-compressed files. It's not really just about the size of the file (although, yeah ... some files get larger when compressed again) ... it's really about the total time to display (network transfer + decompression). Sure, there's a tradeoff, and I don't dispute that there probably are cases with certain assets that double compression might be faster. However, the (old skool πŸ‘΄) guidance is to generally avoid serving double-compressed assets. If a dev wants to do that, they can just modify the file how they see fit.

I've updated the file up there ☝️ with what I'll put on the PR.

I'll put the PR up now and ask Sourabh and Javier to take a look. I'll ping u on the PR.

Hey folks! Wanted to let you know that this sample file presently breaks downloading appsettings.json files for Blazor apps. It seems to be a variant of the https://github.com/dotnet/aspnetcore/issues/26897 issue mentioned earlier. I had to revert my web.config to get my app to work. Any ideas?

presently breaks downloading appsettings.json files for Blazor apps

hummmmmmm πŸ€” ... I don't recall other than to say that the test app didn't flake out on me. No :boom:. Let me open a new issue, and I'll try to look into this tomorrow (Wednesday).

Hey folks! Wanted to let you know that this sample file presently breaks downloading appsettings.json files for Blazor apps. It seems to be a variant of the dotnet/aspnetcore#26897 issue mentioned earlier. I had to revert my web.config to get my app to work. Any ideas?

@robertmclaws The issue raised by that developer was caused by the fact that he was using this web.config file for a Blazor app hosted by ASP.NET, not as a standalone on IIS. If that's your case too, then you shouldn't be using this configuration at all.

Was this page helpful?
0 / 5 - 0 ratings