Avoid returning index.html for API calls

While writing an asp.net hosted Blazor WASM app, I’ve noticed that when I call an API on my server I receive the contents of index.html when I am expecting a 404 error.

TL;DR

app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseAuthentication();
app.UseAuthorization();
// Register your routes here 
app.UseStaticFiles();

// Insert this block of code
app.MapFallback(
	pattern: "/api/{*path}",
	handler: (HttpContext context) =>
	{
		context.Response.StatusCode = 404;
		context.Response.ContentType = "text/plain";
		return context.Response.WriteAsync("404 Not Found");
	});

app.MapFallbackToFile("index.html");
app.Run();

Explanation

When you first request a URL from the host and nothing is found at that address, the server will serve the index.html page using the app.MapFallbackToFile("index.html") line of code.

The reason for this is that the page requested might not be a server page, but a page within the Blazor WASM client (a component decorated with @page or [Route]). The server will serve the index.html page, Blazor will start, and then the Blazor Router will decide whether to show a page, or Not Found.

The problem with this approach is that it assumes every request to the server is a human initiated one (i.e. a link followed by a human, or an address typed into the browser’s address bar). When the call is initiated by our Blazor WASM app we actually want to receive a 404 instead of a 200 with the contents of index.html. This is because our API calls typically expect strongly formatted data such as JSON, so when we try to deserialise HTML as JSON our app crashes.

MapFallbackToFile does have an overloaded method that also accepts a path argument but, unfortunately, the pattern does not allow negative matches (e.g. “any request with a path not starting with /api/”). The solution is to map an additional fallback after our routes have been defined, and before MapFallbackToFile that returns index.html. The pattern of the fallback is the magic string used by asp.net {*path:nonfile} but only when the path starts with our API path /api/.

We don’t actually need the nonfile part, because we know there are no files in our /api path, so the pattern we need is /api/{*path}, which means

  1. Starting with /api/
  2. Followed by any number of any characters (*)
  3. The * is captured as path, hence {*path}

Note that although we do not use path, we must specify it as a capture so the asp.net router knows we are looking for “any number of characters” rather than a literal *.

Summary

We need to keep MapFallbackToFile in order for client-side routing to do what the user expects when the page is first requested by the browser. Unlike a RegEx, we cannot specify a negative pattern to prevent it from mapping to the fallback file for API calls, but we can put another MapFallback before it that positively matches /api calls and returns a 404.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *