angularasp.net-corebrowser-cache

Cache busting: What encoding is used for the hash part of Angular file names?


In previous versions of angular, hashed file names looked like this:

runtime.59772ce71fd8c096.js
polyfills.efe34593e33fb8e1.js
scripts.109c3e02aa410456.js
main.3645a30dcf6ebeb0.js

They were composed by a name followed by a dot, then 16 characters corresponding to the hexadecimal encoding of the hash, then the extension.

Now they look like this:

styles-GCSEUS35.css
chunk-NLN56U2H.js
polyfills-UCPGG4LZ.js
main-UFPD5ECZ.js

They are composed by a name followed by an hyphen, then the hash encoded in 8 characters, ending with the extension. They somehow look as base32, but base32 Crockford doesn't include the "U" character.

I just need to know what characters can be included in the hash part of the file.

The reason I need it is cache busting.

If I can figure out in my server code that a file is a hashed Angular file, I can send cache headers to the browser that allows it to cache the file for a long time without revalidation.

This applies to ASP.NET Core backends that serve static Angular files and need proper server-side cache control.


Solution

  • I still wonder how Angular encodes hashed file names, but I’ve written a middleware that works reliably in production.

    It aggressively caches hashed Angular file names served by an ASP.NET Core backend.

    The goal is to detect which files include a content-based hash in their name (like main-UFPD5ECZ.js) and apply long-term caching headers to them, while leaving others with standard no-cache behavior.

    This is the code:

    public class CachingMiddleware
    {
    
        private static readonly HashSet<char> hashCharacters = new HashSet<char> { 
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
            'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
            'W', 'X', 'Y', 'Z'
        };
    
    
        private readonly RequestDelegate next;
        private readonly ILogger<CachingMiddleware> logger;
        public CachingMiddleware(RequestDelegate next, ILogger<CachingMiddleware> logger)
        {
            this.next = next;
            this.logger = logger;
        }
    
        private bool isHashedName(string name)
        {
            var tokens = name.Split('-');
            if (tokens.Length == 1) return false;
            var hashCandidate = tokens[tokens.Length - 1];
            return hashCandidate.Length == 8 && hashCandidate.All(x => hashCharacters.Contains(x));
        }
    
        public async Task InvokeAsync(HttpContext context)
        {
            string path = context.Request.Path;
            var typedHeaders = context.Response.GetTypedHeaders();
            var fileName = Path.GetFileName(path);
            var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
            var extension = Path.GetExtension(fileName);
            if (extension != "." && (isHashedName(fileNameWithoutExtension)))
            {
                typedHeaders.CacheControl = new CacheControlHeaderValue
                {
                    Public = true,
                    MaxAge = TimeSpan.FromDays(180)
                };
            }
            else
            {
                typedHeaders.CacheControl = new CacheControlHeaderValue
                {
                    Public = false,
                    NoCache = true
                };
            }
            await next(context);
        }
    }
    

    This middleware sits in the pipeline before serving static files. It analyzes the filename and, if it matches the heuristic (a -HASH suffix with 8 base36 characters), it adds Cache-Control: public, max-age=180d. Otherwise, it sets no-cache.

    It works well in real-world Angular 17+ apps where filenames look like:

    main-UFPD5ECZ.js
    styles-GCSEUS35.css