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
- Starting with
/api
/ - Followed by any number of any characters (
*
) - The
*
is captured aspath
, 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