dart-lang / dart-lang/webdev

feat: Add --spa-fallback flag to support client-side routing

Open
#2,655 1 comment 0 reactions 0 assignees View on GitHub
P2 package:webdev triaged type-enhancement
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.