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.
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