feat: Add --spa-fallback flag to support client-side routing
- Dominant language
- Dart
- Stars
- 224
- Forks
- 94
- Avg merge
- 4d 3h
- Merged PRs (30d)
- 2
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:
```sh
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.
```dart
if (options.configuration.spaFallback) {
FutureOr 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.
Contributor guide
Research direction
Start at lib/src/command/serve_command.dart, then follow spaFallback through lib/src/command/configuration.dart into lib/src/serve/webdev_server.dart. Run the serve command and verify that --spa-fallback serves index.html for extensionless GET 404s while asset requests, non-GET requests, and missing index.html remain 404.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- cli, web-dev
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100