Visitar URL original
[flutter_tools] Pause web server requests until initial compilation f… · flutter/flutter@625c5c9 · GitHub
Skip to content

Commit 625c5c9

Browse files
authored
[flutter_tools] Pause web server requests until initial compilation finishes (#191990)
## Description When launching a web application using `flutter run -d web-server`, the HTTP server begins listening as soon as `WebDevFS.create()` starts `WebAssetServer`. However, application compilation in `_updateDevFS()` has not yet completed. If a client requests the page before compilation finishes, DWDS attempts to read module metadata while `_webMemoryFS.mergedMetadata` is still null. DWDS then injects an empty module list into the bootstrap script (`main_module.bootstrap.js`) and permanently memoizes that 0 modules exist in `MetadataProvider._metadataMemoizer`, causing the page to only load `dart_sdk.js` and hang on a blank white screen. In Chrome console logs, this exhibits as `DDC is about to load 1/2 scripts with pool size = 1000` with nothing else loading afterwards. Additionally, if the client connects to DWDS before `webDevFS.connect()` starts listening, the `connectedApps` broadcast event is dropped without listeners, causing the tool to hang waiting for a connection. This PR adds a readiness completer to `WebAssetServer` and a Shelf middleware to pause incoming HTTP requests until initial compilation completes and the DWDS connection listener is attached. ## Related Issues Fixes #191957 ## Tests - Added tests in `packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart` verifying that `isReady` completes on `markReady()`/`dispose()` and that incoming HTTP requests are held until `markReady()` is called. - Added test in `packages/flutter_tools/test/general.shard/resident_web_runner_test.dart` verifying that `ResidentWebRunner.run()` marks `WebDevFS` as ready before starting the app.
1 parent 0116c83 commit 625c5c9

6 files changed

Lines changed: 204 additions & 2 deletions

File tree

‎packages/flutter_tools/lib/src/isolated/devfs_web.dart‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,16 @@ class WebDevFS implements DevFS {
243243
return baseUri;
244244
}
245245

246+
/// Signal that the underlying web asset server is ready to handle requests.
247+
///
248+
/// The HTTP server starts listening early during [create] so the port and URI
249+
/// are known before compilation. However, incoming HTTP requests are held
250+
/// until initial compilation completes and the DWDS connection listener is
251+
/// registered so clients do not load incomplete assets or miss connection events.
252+
void markReady() {
253+
webAssetServer.markReady();
254+
}
255+
246256
@override
247257
Future<void> destroy() async {
248258
await webAssetServer.dispose();

‎packages/flutter_tools/lib/src/isolated/resident_web_runner.dart‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,13 @@ class ResidentWebRunner extends ResidentRunner {
367367
final Future<ConnectionResult?>? connectDebug = supportsServiceProtocol
368368
? webDevFS.connect(useDebugExtension)
369369
: null;
370+
// Unpause incoming HTTP requests now that:
371+
// 1. Initial compilation has finished and WebMemoryFS has received the
372+
// compiled modules and merged metadata (preventing DWDS from memoizing
373+
// an empty module list).
374+
// 2. `webDevFS.connect()` has begun listening for connected applications
375+
// so early connections are not dropped.
376+
webDevFS.markReady();
370377
await flutterDevice!.device!.startApp(
371378
package,
372379
mainPath: target,

‎packages/flutter_tools/lib/src/isolated/web_asset_server.dart‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ class WebAssetServer implements AssetReader {
9999
final Map<String, String> _modules;
100100
final Map<String, String> _digests;
101101

102+
final Completer<void> _readyCompleter = Completer<void>();
103+
104+
/// A future that completes when the server is ready to handle requests.
105+
///
106+
/// Requests received before this future completes are paused by middleware.
107+
Future<void> get isReady => _readyCompleter.future;
108+
109+
/// Signal that the server is ready to handle incoming requests.
110+
///
111+
/// This unpauses any HTTP requests that were received while the server was
112+
/// initializing or waiting for initial compilation to complete.
113+
void markReady() {
114+
if (!_readyCompleter.isCompleted) {
115+
_readyCompleter.complete();
116+
}
117+
}
118+
102119
int get selectedPort => _httpServer.port;
103120

104121
/// Given a list of [modules] that need to be loaded, compute module names and
@@ -298,6 +315,13 @@ class WebAssetServer implements AssetReader {
298315
return server;
299316
}
300317

318+
shelf.Handler waitMiddleware(shelf.Handler innerHandler) {
319+
return (shelf.Request request) async {
320+
await server.isReady;
321+
return innerHandler(request);
322+
};
323+
}
324+
301325
// In release builds (or wasm builds) deploy a simpler proxy server.
302326
if (buildInfo.mode != BuildMode.debug || isWasm) {
303327
final releaseAssetServer = ReleaseAssetServer(
@@ -309,9 +333,12 @@ class WebAssetServer implements AssetReader {
309333
basePath: server.basePath,
310334
needsCoopCoep: crossOriginIsolation,
311335
);
336+
final shelf.Handler releaseHandler = const shelf.Pipeline()
337+
.addMiddleware(waitMiddleware)
338+
.addHandler(releaseAssetServer.handle);
312339
runZonedGuarded(
313340
() {
314-
shelf.serveRequests(httpServer!, releaseAssetServer.handle);
341+
shelf.serveRequests(httpServer!, releaseHandler);
315342
},
316343
(Object e, StackTrace s) {
317344
logger.printTrace('Release asset server: error serving requests: $e:$s');
@@ -388,9 +415,12 @@ class WebAssetServer implements AssetReader {
388415
pipeline = pipeline.addMiddleware(proxyMiddleware(proxy, globals.logger));
389416
final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest);
390417
final shelf.Cascade cascade = shelf.Cascade().add(dwds.handler).add(dwdsHandler);
418+
final shelf.Handler serverHandler = const shelf.Pipeline()
419+
.addMiddleware(waitMiddleware)
420+
.addHandler(cascade.handler);
391421
runZonedGuarded(
392422
() {
393-
shelf.serveRequests(httpServer!, cascade.handler);
423+
shelf.serveRequests(httpServer!, serverHandler);
394424
},
395425
(Object e, StackTrace s) {
396426
logger.printTrace('Dwds server: error serving requests: $e:$s');
@@ -574,6 +604,9 @@ class WebAssetServer implements AssetReader {
574604

575605
/// Tear down the http server running.
576606
Future<void> dispose() async {
607+
if (!_readyCompleter.isCompleted) {
608+
_readyCompleter.complete();
609+
}
577610
if (_dwdsInit) {
578611
await dwds.stop();
579612
}

‎packages/flutter_tools/test/general.shard/resident_web_runner_cold_test.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ class FakeWebDevFS extends Fake implements WebDevFS {
231231
Future<Uri> create() async {
232232
return Uri.base;
233233
}
234+
235+
@override
236+
void markReady() {}
234237
}
235238

236239
class FakeWebDevice extends Fake implements Device {

‎packages/flutter_tools/test/general.shard/resident_web_runner_test.dart‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,26 @@ name: my_app
271271
},
272272
);
273273

274+
testUsingContext(
275+
'ResidentWebRunner marks WebDevFS as ready before starting app',
276+
() async {
277+
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
278+
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
279+
setupMocks();
280+
281+
final connectionInfoCompleter = Completer<DebugConnectionInfo>();
282+
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
283+
await connectionInfoCompleter.future;
284+
285+
expect(webDevFS.wasMarkedReady, true);
286+
},
287+
overrides: <Type, Generator>{
288+
FileSystem: () => fileSystem,
289+
ProcessManager: () => processManager,
290+
Pub: ThrowingPub.new,
291+
},
292+
);
293+
274294
testUsingContext(
275295
'Does not crash if the application exits during DDS startup',
276296
() async {
@@ -2477,6 +2497,12 @@ class FakeWebDevFS extends Fake implements WebDevFS {
24772497
late UpdateFSReport report;
24782498

24792499
Uri? mainUri;
2500+
bool wasMarkedReady = false;
2501+
2502+
@override
2503+
void markReady() {
2504+
wasMarkedReady = true;
2505+
}
24802506

24812507
@override
24822508
List<Uri> sources = <Uri>[];

‎packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:dwds/dwds.dart';
68
import 'package:file/memory.dart';
79
import 'package:flutter_tools/src/artifacts.dart';
@@ -10,6 +12,7 @@ import 'package:flutter_tools/src/base/io.dart';
1012
import 'package:flutter_tools/src/base/logger.dart';
1113
import 'package:flutter_tools/src/base/platform.dart';
1214
import 'package:flutter_tools/src/build_info.dart';
15+
import 'package:flutter_tools/src/convert.dart';
1316
import 'package:flutter_tools/src/isolated/release_asset_server.dart';
1417
import 'package:flutter_tools/src/isolated/web_asset_server.dart';
1518
import 'package:flutter_tools/src/web/compile.dart';
@@ -512,6 +515,126 @@ void main() {
512515
expect(server.basePath, isEmpty);
513516
});
514517

518+
testWithoutContext('isReady is completed by markReady() or dispose()', () async {
519+
final WebAssetServer server = await WebAssetServer.start(
520+
null,
521+
null,
522+
false,
523+
false,
524+
false,
525+
BuildInfo.debug,
526+
false,
527+
const DartDevelopmentServiceConfiguration(enable: false),
528+
Uri.base,
529+
null,
530+
crossOriginIsolation: false,
531+
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
532+
webRenderer: WebRendererMode.canvaskit,
533+
isWasm: false,
534+
useLocalCanvasKit: false,
535+
testMode: true,
536+
fileSystem: fileSystem,
537+
logger: BufferLogger.test(),
538+
platform: platform,
539+
);
540+
541+
var serverReady = false;
542+
unawaited(server.isReady.then((_) => serverReady = true));
543+
expect(serverReady, false);
544+
545+
server.markReady();
546+
await pumpEventQueue();
547+
expect(serverReady, true);
548+
549+
final WebAssetServer server2 = await WebAssetServer.start(
550+
null,
551+
null,
552+
false,
553+
false,
554+
false,
555+
BuildInfo.debug,
556+
false,
557+
const DartDevelopmentServiceConfiguration(enable: false),
558+
Uri.base,
559+
null,
560+
crossOriginIsolation: false,
561+
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
562+
webRenderer: WebRendererMode.canvaskit,
563+
isWasm: false,
564+
useLocalCanvasKit: false,
565+
testMode: true,
566+
fileSystem: fileSystem,
567+
logger: BufferLogger.test(),
568+
platform: platform,
569+
);
570+
571+
var server2Ready = false;
572+
unawaited(server2.isReady.then((_) => server2Ready = true));
573+
expect(server2Ready, false);
574+
575+
await server2.dispose();
576+
await pumpEventQueue();
577+
expect(server2Ready, true);
578+
});
579+
580+
testUsingContext(
581+
'pauses requests until markReady() is called',
582+
() async {
583+
fileSystem.file('build/web/index.html')
584+
..createSync(recursive: true)
585+
..writeAsStringSync('hello');
586+
587+
final WebAssetServer server = await WebAssetServer.start(
588+
null,
589+
null,
590+
false,
591+
false,
592+
false,
593+
BuildInfo.release,
594+
false,
595+
const DartDevelopmentServiceConfiguration(enable: false),
596+
Uri.base,
597+
null,
598+
crossOriginIsolation: false,
599+
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
600+
webRenderer: WebRendererMode.canvaskit,
601+
isWasm: true,
602+
useLocalCanvasKit: false,
603+
fileSystem: fileSystem,
604+
logger: BufferLogger.test(),
605+
platform: platform,
606+
);
607+
608+
final client = HttpClient();
609+
try {
610+
final Future<HttpClientResponse> responseFuture = client
611+
.getUrl(Uri.parse('http://localhost:${server.selectedPort}/'))
612+
.then((HttpClientRequest request) => request.close());
613+
614+
var responseReceived = false;
615+
unawaited(responseFuture.then((_) => responseReceived = true));
616+
617+
// Pump events to ensure request is in flight.
618+
await pumpEventQueue();
619+
expect(responseReceived, false);
620+
621+
server.markReady();
622+
623+
final HttpClientResponse response = await responseFuture;
624+
expect(response.statusCode, HttpStatus.ok);
625+
expect(await utf8.decoder.bind(response).join(), 'hello');
626+
} finally {
627+
client.close();
628+
await server.dispose();
629+
}
630+
},
631+
overrides: <Type, Generator>{
632+
FileSystem: () => fileSystem,
633+
Platform: () => platform,
634+
ProcessManager: () => FakeProcessManager.any(),
635+
},
636+
);
637+
515638
testUsingContext(
516639
'serves assets with exact key and does not serve non-source project files as assets',
517640
() async {

0 commit comments

Comments
 (0)