Skip to content

webdev serve freezes after repeated reloads; proxy connections saturate and hang until restart — workaround is HttpClient.maxConnectionsPerHost = null + short idleTimeout #2672

@insinfo

Description

@insinfo

During day-to-day development I reload the app (F5) after each change. After a few minutes of this cycle, webdev serve stops responding: the page stays blank, network requests show as pending/canceled, and one connection appears to remain “stuck”. The only way out is to open a new browser tab and kill/restart webdev.

This looks related to the asset proxy client that webdev uses to talk to the build daemon. When HttpClient.maxConnectionsPerHost is bounded (e.g., 64 or 200), repeated reloads eventually saturate the pool and the server freezes. Setting it to null (unbounded) and using a very short idleTimeout releases sockets quickly and avoids the freeze.

Environment
webdev serve web:8081 --auto refresh --hostname localhost
webdev --version
3.7.1
dart --version
Dart SDK version: 3.6.2 (stable) (Wed Jan 29 01:20:39 2025 -0800) on "windows_x64"

name: new_sali_frontend
version: 5.1.0
publish_to: none

environment:
  sdk: ^3.2.1

dependencies:
  ngdart: 8.0.0-dev.4
  ngrouter: 4.0.0-dev.3
  ngforms: 5.0.0-dev.3  
  http: any  
  js: any  
  chartjs2: any  
  new_sali_core:
    path: ../core
  dart_excel:
   git:
    url: https://github.com/insinfo/dart_excel.git
    ref: main    #branch name          

dev_dependencies:  
  build_runner: ^2.1.2
  build_test: ^2.1.3
  build_web_compilers: ^4.0.0
  lints: ^2.1.0
  test: ^1.24.0
  sass_builder: ^2.2.1
  

// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';

import 'package:build_daemon/data/build_status.dart' as daemon;
import 'package:dds/devtools_server.dart';
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
import 'package:dwds/sdk_configuration.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:http_multi_server/http_multi_server.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
import 'package:shelf_proxy/shelf_proxy.dart';

import '../command/configuration.dart';
import '../util.dart';
import 'chrome.dart';
import 'handlers/favicon_handler.dart';
import 'utils.dart' show findPackageConfigFilePath;

Logger _logger = Logger('WebDevServer');

class ServerOptions {
  final Configuration configuration;
  final int port;
  final String target;
  final int daemonPort;

  ServerOptions(
    this.configuration,
    this.port,
    this.target,
    this.daemonPort,
  );
}

class WebDevServer {
  final HttpServer _server;
  final http.Client _client;
  final String _protocol;

  final Stream<BuildResult> buildResults;

  /// Can be null if client.js injection is disabled.
  final Dwds? dwds;

  final ExpressionCompilerService? ddcService;

  final String target;

  WebDevServer._(
    this.target,
    this._server,
    this._client,
    this._protocol,
    this.buildResults,
    bool autoRun, {
    this.dwds,
    this.ddcService,
  }) {
    if (autoRun) {
      dwds?.connectedApps.listen((connection) {
        connection.runMain();
      });
    }
  }

  String get host => _server.address.host;
  int get port => _server.port;
  String get protocol => _protocol;

  Future<void> stop() async {
    await dwds?.stop();
    await ddcService?.stop();
    await _server.close(force: true);
    _client.close();
  }

  static Future<WebDevServer> start(
      ServerOptions options, Stream<daemon.BuildResults> buildResults) async {
    var pipeline = const Pipeline();

    if (options.configuration.logRequests) {
      pipeline = pipeline.addMiddleware(logRequests());
    }

    pipeline = pipeline.addMiddleware(interceptFavicon);

    // Only provide relevant build results
    final filteredBuildResults = buildResults.asyncMap<BuildResult>((results) {
      final result = results.results
          .firstWhere((result) => result.target == options.target);
      switch (result.status) {
        case daemon.BuildStatus.started:
          return BuildResult((b) => b.status = BuildStatus.started);
        case daemon.BuildStatus.failed:
          return BuildResult((b) => b.status = BuildStatus.failed);
        case daemon.BuildStatus.succeeded:
          return BuildResult((b) => b.status = BuildStatus.succeeded);
        default:
          break;
      }
      throw StateError('Unexpected Daemon build result: $result');
    });

    var cascade = Cascade();
    final client = IOClient(HttpClient()
      ..maxConnectionsPerHost = null // for fix bug pending connections on F5
      ..idleTimeout = const Duration(seconds: 1) // release fast
      ..connectionTimeout = const Duration(seconds: 10));

    // use host from CLI mismatch IPv4/IPv6.
    final proxyHost = (options.configuration.hostname == '0.0.0.0' ||
            options.configuration.hostname == 'localhost')
        ? '127.0.0.1'
        : options.configuration.hostname;

    final assetHandler = proxyHandler(
      'http://$proxyHost:${options.daemonPort}/${options.target}/',
      client: client,
    );

    Dwds? dwds;
    ExpressionCompilerService? ddcService;
    if (options.configuration.enableInjectedClient) {
      final assetReader = ProxyServerAssetReader(
        options.daemonPort,
        root: options.target,
      );

      // TODO(https://github.com/flutter/devtools/issues/5350): Figure out how
      // to determine the build settings from the build.
      // Can we save build metadata in build_web_compilers and and read it in
      // the load strategy?
      final buildSettings = BuildSettings(
        appEntrypoint:
            Uri.parse('org-dartlang-app:///${options.target}/main.dart'),
        canaryFeatures: options.configuration.canaryFeatures,
        isFlutterApp: false,
        experiments: options.configuration.experiments,
      );

      final loadStrategy = BuildRunnerRequireStrategyProvider(
        assetHandler,
        options.configuration.reload,
        assetReader,
        buildSettings,
        packageConfigPath: findPackageConfigFilePath(),
      ).strategy;

      if (options.configuration.enableExpressionEvaluation) {
        ddcService = ExpressionCompilerService(
          options.configuration.hostname,
          options.port,
          verbose: options.configuration.verbose,
          sdkConfigurationProvider: const DefaultSdkConfigurationProvider(),
        );
      }
      final shouldServeDevTools =
          options.configuration.debug || options.configuration.debugExtension;

      final debugSettings = DebugSettings(
        enableDebugExtension: options.configuration.debugExtension,
        enableDebugging: options.configuration.debug,
        spawnDds: !options.configuration.disableDds,
        expressionCompiler: ddcService,
        devToolsLauncher: shouldServeDevTools
            ? (String hostname) async {
                final server = await DevToolsServer().serveDevTools(
                  hostname: hostname,
                  enableStdinCommands: false,
                  customDevToolsPath: devToolsPath,
                );
                return DevTools(server!.address.host, server.port, server);
              }
            : null,
      );

      final appMetadata = AppMetadata(
        hostname: options.configuration.hostname,
      );

      final toolConfiguration = ToolConfiguration(
          loadStrategy: loadStrategy,
          debugSettings: debugSettings,
          appMetadata: appMetadata);
      dwds = await Dwds.start(
        toolConfiguration: toolConfiguration,
        assetReader: assetReader,
        buildResults: filteredBuildResults,
        chromeConnection: () async =>
            (await Chrome.connectedInstance).chromeConnection,
      );
      pipeline = pipeline.addMiddleware(dwds.middleware);
      cascade = cascade.add(dwds.handler);
      cascade = cascade.add(assetHandler);
    } else {
      cascade = cascade.add(assetHandler);
    }

    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',
      //   });
      // }
      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 indexPath = p.join(Directory.current.path, 'web', 'index.html');
        final bytes = await File(indexPath).readAsBytes();
        return Response.ok(bytes, headers: {
          'content-type': 'text/html; charset=utf-8',
          'cache-control': 'no-store, no-cache, must-revalidate',
        });
      }

      cascade = cascade.add(spaFallbackHandler);
    }

    final hostname = options.configuration.hostname;
    final tlsCertChain = options.configuration.tlsCertChain ?? '';
    final tlsCertKey = options.configuration.tlsCertKey ?? '';

    HttpServer server;
    final protocol =
        (tlsCertChain.isNotEmpty && tlsCertKey.isNotEmpty) ? 'https' : 'http';
    if (protocol == 'https') {
      final serverContext = SecurityContext()
        ..useCertificateChain(tlsCertChain)
        ..usePrivateKey(tlsCertKey);
      server = await HttpMultiServer.bindSecure(
          hostname, options.port, serverContext);
    } else {
      server = await HttpMultiServer.bind(hostname, options.port);
    }

    serveHttpRequests(server, pipeline.addHandler(cascade.handler), (e, s) {
      _logger.warning('Error serving requests', e, s);
    });

    return WebDevServer._(
      options.target,
      server,
      client,
      protocol,
      filteredBuildResults,
      options.configuration.autoRun,
      dwds: dwds,
      ddcService: ddcService,
    );
  }
}

//dart c:\MyDartProjects\webdev\webdev\bin\webdev.dart serve --spa-fallback web:8080 --hostname 127.0.0.1 --auto refresh 

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