-
Notifications
You must be signed in to change notification settings - Fork 86
Description
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