Skip to content

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

@insinfo

Description

@insinfo

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions