Serving static index.html from the spa folder - vue.js

I use .netcore server side and vuejs 2.
I have generated html files for some routes of my vuejs, that I placed directly in the dist folder:
I can access the html files with http://my-domain/en/home/index.html, but calling http://my-domain/en/home (without the index.html) won't serve the html file. Instead, it will return the equivalent spa page.
What can I do to fix this? I want the server to return the html file if it exists in priority, otherwise return the normal spa website.
Here is part of my startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// ...
// In production, the vue files will be served from this directory
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
// ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
// WebRootPath == null workaround. - from https://github.com/aspnet/Mvc/issues/6688
if (string.IsNullOrWhiteSpace(env.WebRootPath))
{
env.WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "ClientApp", "dist");
}
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
const int durationInSeconds = 60 * 60 * 24;
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + durationInSeconds;
}
});
app.UseSpaStaticFiles();
// ...
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseVueCli(npmScript: "serve", port: 8085);
}
});
}
EDIT: On top of #Phil 's response, I needed to provide a FileProvider because UseDefaultFiles wasn't looking in the right folder:
app.UseDefaultFiles(new DefaultFilesOptions
{
FileProvider = new PhysicalFileProvider(env.WebRootPath) // important or it doesn't know where to look for
});
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
const int durationInSeconds = 60 * 60 * 24;
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
"public,max-age=" + durationInSeconds;
},
FileProvider = new PhysicalFileProvider(env.WebRootPath) // same as UseDefaultFiles
});

You need to tell the server to use Default Files
With UseDefaultFiles, requests to a folder search for:
default.htm
default.html
index.htm
index.html
app.UseDefaultFiles(); // this must come first
app.UseStaticFiles(...
This basically sets up an interceptor for requests on a folder (like your en/home) and if it finds any of the above filenames, will rewrite the URL to folder/path/{FoundFilename}.
If you want to avoid searches for anything other than index.html, you can customise it
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(options);
Note the important information about ordering
Important
UseDefaultFiles must be called before UseStaticFiles to serve the default file. UseDefaultFiles is a URL rewriter that doesn't actually serve the file. Enable Static File Middleware via UseStaticFiles to serve the file.

Related

ASP.NET Core Web API - Protect some static files with authentication

In my ASP.NET Core 6 Web API, I have a folder named files. And I only want to protect folder files/users with authentication.
In my program.cs I have this:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "files")),
RequestPath = "/files",
});
If I put this on my program.cs all folders are "included".
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Someone can help me to only protect this folder?
you can use OnPrepareResponse as like
app.UseAuthentication();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments("/files/users"))
{
ctx.Context.Response.Headers.Add("Cache-Control", "no-store");
if (!ctx.Context.User.Identity.IsAuthenticated)
{
// respond HTTP 401 Unauthorized with empty body.
ctx.Context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
ctx.Context.Response.ContentLength = 0;
ctx.Context.Response.Body = Stream.Null;
// - or, redirect to another page. -
// ctx.Context.Response.Redirect("/");
}
}
}
}

Blazor Redirection on IIS swagger

I have a .NET 5 blazor WASM (with core server) solution.
I added swagger (nswag) like this:
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
services.AddSwaggerDocument(); //SWAGGER
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if(env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else {
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
// Register the Swagger generator and the Swagger UI middlewares
app.UseOpenApi();
app.UseSwaggerUi3();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
When I debug the appliation with IIS-Express and enter the address https://localhost:12234/swagger the swagger UI is displayed correctly.
But after deployment to IIS every address loads the blazor UI with "Sorry there is nothing at this address" instead of the swagger UI.
When I use an old IE (not able to run wasm) I get at least a title from swagger - so swagger is there on the server, but some "magic redirection" forces index.html to be loaded - no matter what I do.
By the way - I can call controller methods and a curl .../swagger/v1/swagger.json also works as expected.
How can I tell the app to accept URLs from the address line without redirection to index.html?
I use PWA and https in my project.
I found the solution.
There is a service-worker.published.js as a "subfile" in wwwroot/service-worker.js.
And there is code like this:
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate';
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}
After a little change everthing works fine now:
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate' && !event.request.url.includes('/swagger') && !event.request.url.includes('/api/');
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}
Adding two more conditions to shouldServeIndexHtml solved the problem.
const shouldServeIndexHtml = event.request.mode === 'navigate' && !event.request.url.includes('/swagger') && !event.request.url.includes('/api/');

How to cache static files in ASP.NET Core?

I can't seem to enable caching of static files in ASP.NET Core 2.2. I have the following in my Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseCors(...);
}
else {
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseSignalR(routes => { routes.MapHub<NotifyHub>("/..."); });
app.UseResponseCompression();
app.UseStaticFiles();
app.UseSpaStaticFiles(new StaticFileOptions() {
OnPrepareResponse = (ctx) => {
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public, max-age=31557600"; // cache for 1 year
}
});
app.UseMvc();
app.UseSpa(spa => {
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment()) {
spa.UseVueCli(npmScript: "serve", port: 8080);
}
});
}
When I try and Audit the production site on HTTPS using chrome I keep getting "Serve static assets with an efficient cache policy":
In the network tab there is no mention of caching in the headers, when I press F5 it seems everything is served from disk cache. But, how can I be sure my caching setting is working if the audit is showing its not?
This is working in ASP.NET Core 2.2 to 3.1:
I know this is a bit similar to Fredrik's answer but you don't have to type literal strings in order to get the cache control header
app.UseStaticFiles(new StaticFileOptions()
{
HttpsCompression = Microsoft.AspNetCore.Http.Features.HttpsCompressionMode.Compress,
OnPrepareResponse = (context) =>
{
var headers = context.Context.Response.GetTypedHeaders();
headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(30)
};
}
});
I do not know what UseSpaStaticFiles is but you can add cache options in UseStaticFiles. You have missed to set an Expires header.
// Use static files
app.UseStaticFiles(new StaticFileOptions {
OnPrepareResponse = ctx =>
{
// Cache static files for 30 days
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
}
});
Beware that you also need a way to invalidate cache when you make changes to static files.
I have written a blog post about this: Minify and cache static files in ASP.NET Core

Client Side Deep Links with WebpackDevMiddleware 404s

I am using the WebpackDevMiddleware for Development builds to serve up a Vue.js application that uses client-side routing. The SPA application is served up from the root url just fine, but if I attempt to use any client-side deep links, I get a 404.
Note running as Production works as expected.
What I want:
http://locahost/ - serve up the vue app.
http://localhost/overlays/chat - serve up the vue app.
http://localhost/api/* - serve up the api routes handled server side.
There is a minimum viable reproduction of the problem in this repository. You can run it using vscode debugging as Development environment where the bug happens. There is also a script /scripts/local-production that will build and run as Production environment, where it works as expected.
Relevant portions of my Startup.cs looks like this:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// In production, the Vue files will be served
// from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = Configuration["Client"];
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//set up default mvc routing
app.UseMvc(routes =>
{
routes.MapRoute("default", "api/{controller=Home}/{action=Index}/{id?}");
});
//setup spa routing for both dev and prod
if (env.IsDevelopment())
{
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true,
ProjectPath = Path.Combine(env.ContentRootPath, Configuration["ClientProjectPath"]),
ConfigFile = Path.Combine(env.ContentRootPath, Configuration["ClientProjectConfigPath"])
});
}
else
{
app.UseWhen(context => !context.Request.Path.Value.StartsWith("/api"),
builder => {
app.UseSpaStaticFiles();
app.UseSpa(spa => {
spa.Options.DefaultPage = "/index.html";
});
app.UseMvc(routes => {
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Fallback", action = "Index" });
});
});
}
}
}
I was able to get around this using the status code pages middleware to handle all status codes and re-execute using the root path. This will cause the spa app to be served up for all status codes in the 400-599 range which is not quite what I want but gets me working again at least.
//setup spa routing for both dev and prod
if (env.IsDevelopment())
{
//force client side deep links to render the spa on 404s
app.UseStatusCodePagesWithReExecute("/");
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true,
ProjectPath = Path.Combine(env.ContentRootPath, Configuration["ClientProjectPath"]),
ConfigFile = Path.Combine(env.ContentRootPath, Configuration["ClientProjectConfigPath"])
});
}
Hopefully, this will help someone in the future that might be bumping up against this issue.

Asp.net core 2.1 - How to serve multiple angular apps?

I'm trying to serve 2 angular apps from my .net core service like this:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "wwwroot/app";
});
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "wwwroot/admin";
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
app.UseSpaStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=0");
}
});
app.UseMvc();
app.Map("/app", client =>
{
client.UseSpa(spa =>
{
spa.Options.SourcePath = "wwwroot/app";
});
}).Map("/admin", admin =>
{
admin.UseSpa(spa =>
{
spa.Options.SourcePath = "wwwroot/admin";
});
});
}
in the file system, I only have the dist output of these apps (since they are developed by another team). So it looks like this:
C:[MY PROJECT PATH]\wwwroot\app
C:[MY PROJECT PATH]\wwwroot\admin
But for some reason, the admin app works and the app doesn't and also it doesn't support a default page so I need to enter the URL with /index.html.
Any ideas on how to solve this?
Well, I finally solved it and it works like this:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSpaStaticFiles();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
app.UseSpaStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"))
});
app.UseMvc();
app.Map("/app", client =>
{
client.UseSpa(spa =>
{
spa.Options.SourcePath = "wwwroot/app";
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/app"))
};
});
}).Map("/admin", admin =>
{
admin.UseSpa(spa =>
{
spa.Options.SourcePath = "wwwroot/admin";
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/admin"))
};
});
});
}
Also, don't forget to go into the index.html file of each app and set the base tag accordingly like this:
//for the client application:
<base href="/app/">
//for the admin application
<base href="/admin/">
Liran answered his own question.
In my case I had multiple entry points for a vue cli site. The fix is identical to the correct answer, but also setting spa.Options.DefaultPage.
app
.Map("/login", admin =>
{
admin.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.Options.DefaultPage = "/login.html";
});
})
.Map("", client =>
{
client.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.Options.DefaultPage = "/index.html";
});
});