-
Notifications
You must be signed in to change notification settings - Fork 85
Description
The Problem
Currently, webdev serve
does not natively support Single Page Applications (SPAs) that use the HTML5 History API for client-side routing (like Angular's PathLocationStrategy
or React Router). When a developer is on a "deep link" URL such as /users/123
and refreshes the page, the webdev
server correctly returns a 404 error, as there is no file at that path.
This breaks the development workflow and forces developers to navigate back to the root of the application manually after every refresh on a nested route.
Proposed Solution
This change introduces a new command-line flag, --spa-fallback
, to the serve
command.
When this flag is enabled, the server's behavior is modified: any GET
request for a path that does not have a file extension (i.e., it's not a request for an asset like .js
, .css
, or .png
) and would normally result in a 404 will instead serve the root index.html
file.
This allows the client-side application's JavaScript to load, inspect the browser's current URL, and let the client-side router handle displaying the correct component or view.
How to Use
The new feature can be enabled by running:
dart run webdev serve --spa-fallback
Implementation Details
This was implemented by:
Adding a new --spa-fallback boolean flag to lib/src/command/serve_command.dart.
Propagating this flag through lib/src/command/configuration.dart.
In lib/src/serve/webdev_server.dart, conditionally adding a shelf handler to the Cascade that intercepts 404s for non-asset paths and serves index.html.
if (options.configuration.spaFallback) {
FutureOr<Response> spaFallbackHandler(Request request) async {
final hasExt = request.url.pathSegments.isNotEmpty &&
request.url.pathSegments.last.contains('.');
if (request.method != 'GET' || hasExt) {
return Response.notFound('Not Found');
}
final indexUri =
request.requestedUri.replace(path: 'index.html', query: '');
final cleanHeaders = Map.of(request.headers)
..remove('if-none-match')
..remove('if-modified-since');
final proxiedReq = Request(
'GET',
indexUri,
headers: cleanHeaders,
context: request.context,
protocolVersion: request.protocolVersion,
);
final resp = await assetHandler(proxiedReq);
if (resp.statusCode != 200 && resp.statusCode != 304) {
return Response.notFound('Not Found');
}
return resp.change(headers: {
...resp.headers,
'content-type': 'text/html; charset=utf-8',
});
}
cascade = cascade.add(spaFallbackHandler);
}
This feature makes webdev a more powerful and convenient tool for modern web development with Dart.