As reported by @madnik7 on Slack, EmbedIO doesn't have good support for SPAs with client-side routing, i.e. where there's only one HTML "entry point" page mapping paths to views via Javascript code.
If users always visit the home page first, there's no problem, as when they click a link to, say /login, it gets processed on the client side and no request ever reaches the server. There's no actual login.html file to serve; instead, the client-side router just loads and activates a "login" view.
The problems start when a user wants to load a view directly. Say I save a bookmark to my /dashboard page and later open my browser and click it:
the URL path doesn't start with either /api or any other path served by "functional" modules, so the requests slips path them;
the request finally reaches the FileModule serving the app's static files;
neither /dashboard nor /dashboard.html (in case there's a DefaultExtension set) are found by the file provider;
error 404!
As of version 3.4.3, the only workaround is to provide an OnMappingFailed callback that redirects requests to /, so at least the user sees something (namely the home page) they can navigate from. This is clearly not enough: the user explicitly requested the /dashboard page, so that's what they should get.
Other servers (Apache, ASP.NET Core applications, _insert_your_preferencehere) have no problem handling this situation, but it's a show-stopper for EmbedIO.
Proposed enhancement
When the requested URL path maps to a view in a SPA, the "main" HTML file should be served with no redirection occurring whatsoever, so the client-side code can see the path and act accordingly.
Or, to reformulate in a less application-specific fashion: an EmbedIO application should be able to decide which path is actually requested to the Provider of a FileModule, based upon the HTTP context's RequestedPath (plus possibly other factors, e.g. context Items).
Implementation proposals
Add a PreProcessPath callback to FileModule, whose signature is define by the following delegate:
using System.Threading.Tasks;
namespace EmbedIO.Files
{
/// <summary>
/// A callback used to obtain the actual path to be served by a <see cref="FileModule"/>, based upon the requested path
/// (and other context data if desired).
/// </summary>
/// <param name="context">An <see cref="IHttpContext"/> interface representing the context of the request.</param>
/// <returns>The path to serve.</returns>
public delegate string FilePreProcessPathCallback(IHttpContext context);
}
The default callback should just return context.RequestedPath, thus replicating current behavior.
Usage examples
Let's assume we have a SPA with client-side routing, where paths (if not otherwise served by other modules) map to actual files if they have an extension, or to views if they have no extension. This is a rather common use case; other SPAs may have simpler rules (maybe an array of view names, if they are less than a dozen and not bound to change often) or more complex ones, but all it takes to support them is different code in the PreProcessPath callback.
Let's also assume that the SPA has a /404 view that shows an appropriate "not found" message.
var server = new WebServer(o => o
.WithUrlPrefix(url)
.WithMode(HttpListenerMode.EmbedIO))
.WithLocalSessionManager()
.WithWebApi("/api", m => m
.WithController<MyWebApiController>())
.WithModule(new MyWebSocketModule("/ws"))
.WithStaticFolder("/", HtmlRootPath, true, m => m
.WithContentCaching(UseFileCache)
.WithPreProcessPath(ctx => {
var path = ctx.RequestedPath;
return string.IsNullOrEmpty(Path.GetExtension(path)) ? "/index.html" : path;
}))
.WithModule(new RedirectModule("/", "/404"));
Risks
If a PreProcessPath callback takes some time to complete, it can have a negative performance impact on the FileModule it is attached to. I defined it as synchronous (it returns string, not Task<string>) so hopefully people won't get weird ideas, such as querying a database with a table of view names.
If EmbedIO gains more traction because of better support for SPAs, we may have to cope with even more issues being opened about HttpListener's shortcomings. OK, just kidding... maybe.
Background and motivation
As reported by @madnik7 on Slack, EmbedIO doesn't have good support for SPAs with client-side routing, i.e. where there's only one HTML "entry point" page mapping paths to views via Javascript code.
If users always visit the home page first, there's no problem, as when they click a link to, say
/login
, it gets processed on the client side and no request ever reaches the server. There's no actuallogin.html
file to serve; instead, the client-side router just loads and activates a "login" view.The problems start when a user wants to load a view directly. Say I save a bookmark to my
/dashboard
page and later open my browser and click it:/api
or any other path served by "functional" modules, so the requests slips path them;FileModule
serving the app's static files;/dashboard
nor/dashboard.html
(in case there's aDefaultExtension
set) are found by the file provider;As of version 3.4.3, the only workaround is to provide an
OnMappingFailed
callback that redirects requests to/
, so at least the user sees something (namely the home page) they can navigate from. This is clearly not enough: the user explicitly requested the/dashboard
page, so that's what they should get.Other servers (Apache, ASP.NET Core applications, _insert_your_preferencehere) have no problem handling this situation, but it's a show-stopper for EmbedIO.
Proposed enhancement
When the requested URL path maps to a view in a SPA, the "main" HTML file should be served with no redirection occurring whatsoever, so the client-side code can see the path and act accordingly.
Or, to reformulate in a less application-specific fashion: an EmbedIO application should be able to decide which path is actually requested to the
Provider
of aFileModule
, based upon the HTTP context'sRequestedPath
(plus possibly other factors, e.g. contextItems
).Implementation proposals
Add a
PreProcessPath
callback toFileModule
, whose signature is define by the following delegate:The default callback should just return
context.RequestedPath
, thus replicating current behavior.Usage examples
Let's assume we have a SPA with client-side routing, where paths (if not otherwise served by other modules) map to actual files if they have an extension, or to views if they have no extension. This is a rather common use case; other SPAs may have simpler rules (maybe an array of view names, if they are less than a dozen and not bound to change often) or more complex ones, but all it takes to support them is different code in the
PreProcessPath
callback.Let's also assume that the SPA has a
/404
view that shows an appropriate "not found" message.Risks
PreProcessPath
callback takes some time to complete, it can have a negative performance impact on theFileModule
it is attached to. I defined it as synchronous (it returnsstring
, notTask<string>
) so hopefully people won't get weird ideas, such as querying a database with a table of view names.HttpListener
's shortcomings. OK, just kidding... maybe.