I've created a React-based SPA that runs on ASP.NET Core 5.0 but recently encountered an issue after pushing an update with breaking changes to production - users are often seeing errors when trying to access the site because their browser is loading a stale version of the SPA that's incompatible with the changes made to the backend.
Investigation
Digging into this, it seems like the problem is that index.html
is being cached for longer than it should and that I should be configuring it to never be cached, based on what I read in these posts:
I came across a person having a similar issue to mine in the post here and followed that through to this solution. After a bit more digging, I also came across a more comprehensive version of that solution in this Github thread and I've based my current attempt on that.
Similar Questions
I found this post that's similar to mine: StaticFileOptions.OnPrepareResponse does not get called for index.html. The difference is that their callback was hit for everything except the the path they were interested in, whereas my callback is never hit at all.
Current Solution Attempt
In my application, I've changed my Startup.Configure
method to this:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();
app.UseSpaStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments("/static"))
{
// Cache all static resources for 1 year (versioned filenames)
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(365),
};
}
else
{
// Do not cache explicit `/index.html` or any other files.
// See also: `DefaultPageStaticFileOptions` below for implicit "/index.html"
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(0),
};
}
},
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "spa";
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
OnPrepareResponse = ctx => {
// Do not cache implicit `/index.html`. See also: `UseSpaStaticFiles` above
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(0),
};
},
};
if (Environment.GetEnvironmentVariable("LOCAL_DEVELOPMENT") == "1")
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
However, I find that neither OnPrepareResponse
callback seems to be invoked when my SPA loads. If I put a breakpoint in either callback, neither of them get hit. In case it's just an issue with the debugger I made both callbacks throw exceptions but that had no impact on behaviour either. This user at the bottom of the GitHub thread seemed to have a similar problem to me, but he never got a response.
Minimal Code Reproduction
I've tried to minimally reproduce this by creating a new project from the Microsoft SPA templates and adding in the same code as above. However, I still don't see either callback getting invoked in this codebase either. These are the steps I took to set up this minimal codebase:
- Open a PowerShell window
- Run
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
- Run
dotnet new reactredux --output temp-spa
- Set the
OnPrepareResponse
properties withinapp.UseSpaStaticFiles()
andapp.UseSpa()
to a simple callback that I could put a breakpoint orDebug.WriteLine()
statement in.
I'm using dotnet v6.0.101 and have node v16.3.0 and npm v7.15.1 installed.
Interim Fix
What I have found works is to add the following middleware delegate somewhere before app.UseSpa
:
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
var requestPath = context.Request.Path.Value!;
if (requestPath.Equals("/"))
{
context.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
context.Response.Headers.Add("Pragma", "no-cache");
context.Response.Headers.Add("Expires", "0");
}
return Task.FromResult(0);
});
await next();
});
When the SPA tries to retrieve the index.html
file (at path /
), I can see this callback getting invoked and that the server's response contains these cache-control headers:
However, I'd prefer to achieve this via SpaStaticFiles
and I'd like to understand why those callbacks aren't being called.
Update
Reviewing the SPA Static Files Extension source code here, it seems that I'm observing this behaviour because no static file is provided when serving the files using the React Development Server.
I've confirmed that this is the case while stepping into that code with source link enabled:
I'll play around and see if there's a way to get the web server to serve the files using the SPA static file middleware while I'm debugging the app locally.