Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit b1b1ee9

Browse files
authored
[web] Fix JS crash when FF blocks service workers. (#106072)
1 parent 2c15e3c commit b1b1ee9

File tree

5 files changed

+160
-3
lines changed

5 files changed

+160
-3
lines changed

dev/bots/service_worker_test.dart

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ final String _testAppWebDirectory = path.join(_testAppDirectory, 'web');
2121
final String _appBuildDirectory = path.join(_testAppDirectory, 'build', 'web');
2222
final String _target = path.join('lib', 'service_worker_test.dart');
2323
final String _targetWithCachedResources = path.join('lib', 'service_worker_test_cached_resources.dart');
24+
final String _targetWithBlockedServiceWorkers = path.join('lib', 'service_worker_test_blocked_service_workers.dart');
2425
final String _targetPath = path.join(_testAppDirectory, _target);
2526

2627
enum ServiceWorkerTestType {
28+
blockedServiceWorkers,
2729
withoutFlutterJs,
2830
withFlutterJs,
2931
withFlutterJsShort,
@@ -37,6 +39,7 @@ Future<void> main() async {
3739
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
3840
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
3941
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
42+
await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false);
4043
}
4144

4245
Future<void> _setAppVersion(int version) async {
@@ -52,6 +55,9 @@ Future<void> _setAppVersion(int version) async {
5255
String _testTypeToIndexFile(ServiceWorkerTestType type) {
5356
late String indexFile;
5457
switch (type) {
58+
case ServiceWorkerTestType.blockedServiceWorkers:
59+
indexFile = 'index_with_blocked_service_workers.html';
60+
break;
5561
case ServiceWorkerTestType.withFlutterJs:
5662
indexFile = 'index_with_flutterjs.html';
5763
break;
@@ -562,3 +568,89 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
562568

563569
print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n');
564570
}
571+
572+
Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
573+
required bool headless
574+
}) async {
575+
final Map<String, int> requestedPathCounts = <String, int>{};
576+
void expectRequestCounts(Map<String, int> expectedCounts) =>
577+
_expectRequestCounts(expectedCounts, requestedPathCounts);
578+
579+
AppServer? server;
580+
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async =>
581+
_waitForAppToLoad(waitForCounts, requestedPathCounts, server);
582+
583+
Future<void> startAppServer({
584+
required String cacheControl,
585+
}) async {
586+
final int serverPort = await findAvailablePort();
587+
final int browserDebugPort = await findAvailablePort();
588+
server = await AppServer.start(
589+
headless: headless,
590+
cacheControl: cacheControl,
591+
// TODO(yjbanov): use a better port disambiguation strategy than trying
592+
// to guess what ports other tests use.
593+
appUrl: 'http://localhost:$serverPort/index.html',
594+
serverPort: serverPort,
595+
browserDebugPort: browserDebugPort,
596+
appDirectory: _appBuildDirectory,
597+
additionalRequestHandlers: <Handler>[
598+
(Request request) {
599+
final String requestedPath = request.url.path;
600+
requestedPathCounts.putIfAbsent(requestedPath, () => 0);
601+
requestedPathCounts[requestedPath] = requestedPathCounts[requestedPath]! + 1;
602+
if (requestedPath == 'CLOSE') {
603+
return Response.ok('OK');
604+
}
605+
return Response.notFound('');
606+
},
607+
],
608+
);
609+
}
610+
611+
// Preserve old index.html as index_og.html so we can restore it later for other tests
612+
await runCommand(
613+
'mv',
614+
<String>[
615+
'index.html',
616+
'index_og.html',
617+
],
618+
workingDirectory: _testAppWebDirectory,
619+
);
620+
621+
print('BEGIN runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)\n');
622+
try {
623+
await _rebuildApp(version: 1, testType: ServiceWorkerTestType.blockedServiceWorkers, target: _targetWithBlockedServiceWorkers);
624+
625+
print('Ensure app starts (when service workers are blocked)');
626+
await startAppServer(cacheControl: 'max-age=3600');
627+
await waitForAppToLoad(<String, int>{
628+
'CLOSE': 1,
629+
});
630+
expectRequestCounts(<String, int>{
631+
'index.html': 1,
632+
'flutter.js': 1,
633+
'main.dart.js': 1,
634+
'assets/FontManifest.json': 1,
635+
'assets/fonts/MaterialIcons-Regular.otf': 1,
636+
'CLOSE': 1,
637+
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
638+
if (!headless)
639+
...<String, int>{
640+
'manifest.json': 1,
641+
'favicon.ico': 1,
642+
},
643+
});
644+
} finally {
645+
await runCommand(
646+
'mv',
647+
<String>[
648+
'index_og.html',
649+
'index.html',
650+
],
651+
workingDirectory: _testAppWebDirectory,
652+
);
653+
await server?.stop();
654+
}
655+
print('END runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)\n');
656+
}

dev/bots/test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,7 @@ Future<void> _runWebLongRunningTests() async {
10871087
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
10881088
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
10891089
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
1090+
() => runWebServiceWorkerTestWithBlockedServiceWorkers(headless: true),
10901091
() => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),
10911092
() => _runWebStackTraceTest('release', 'lib/stack_trace.dart'),
10921093
() => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'),
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:html' as html;
6+
Future<void> main() async {
7+
const String response = 'CLOSE?version=1';
8+
await html.HttpRequest.getString(response);
9+
html.document.body?.appendHtml(response);
10+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE HTML>
2+
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
3+
Use of this source code is governed by a BSD-style license that can be
4+
found in the LICENSE file. -->
5+
<html>
6+
<head>
7+
<meta charset="UTF-8">
8+
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
9+
10+
<title>Web Test</title>
11+
<!-- iOS meta tags & icons -->
12+
<meta name="apple-mobile-web-app-capable" content="yes">
13+
<meta name="apple-mobile-web-app-status-bar-style" content="black">
14+
<meta name="apple-mobile-web-app-title" content="Web Test">
15+
<link rel="manifest" href="manifest.json">
16+
17+
<script>
18+
// This is to break the serviceWorker registration, and make it throw a DOMException!
19+
// Test for issue https://github.com/flutter/flutter/issues/103972
20+
window.navigator.serviceWorker.register = (url) => {
21+
console.error('Failed to register/update a ServiceWorker for scope ', url,
22+
': Storage access is restricted in this context due to user settings or private browsing mode.');
23+
return Promise.reject(new DOMException('The operation is insecure.'));
24+
}
25+
</script>
26+
27+
<script>
28+
// The value below is injected by flutter build, do not touch.
29+
var serviceWorkerVersion = null;
30+
</script>
31+
<!-- This script adds the flutter initialization JS code -->
32+
<script src="flutter.js" defer></script>
33+
</head>
34+
<body>
35+
<script>
36+
window.addEventListener('load', function(ev) {
37+
// Download main.dart.js
38+
_flutter.loader.loadEntrypoint({
39+
serviceWorker: {
40+
serviceWorkerVersion: serviceWorkerVersion,
41+
}
42+
}).then(function(engineInitializer) {
43+
return engineInitializer.autoStart();
44+
});
45+
});
46+
</script>
47+
</body>
48+
</html>

packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ _flutter.loader = null;
7676
7777
_loadEntrypoint(entrypointUrl) {
7878
if (!this._scriptLoaded) {
79+
console.debug("Injecting <script> tag.");
7980
this._scriptLoaded = new Promise((resolve, reject) => {
8081
let scriptTag = document.createElement("script");
8182
scriptTag.src = entrypointUrl;
@@ -96,7 +97,7 @@ _flutter.loader = null;
9697
_waitForServiceWorkerActivation(serviceWorker, entrypointUrl) {
9798
if (!serviceWorker || serviceWorker.state == "activated") {
9899
if (!serviceWorker) {
99-
console.warn("Cannot activate a null service worker. Falling back to plain <script> tag.");
100+
console.warn("Cannot activate a null service worker.");
100101
} else {
101102
console.debug("Service worker already active.");
102103
}
@@ -114,7 +115,7 @@ _flutter.loader = null;
114115
115116
_loadWithServiceWorker(entrypointUrl, serviceWorkerOptions) {
116117
if (!("serviceWorker" in navigator) || serviceWorkerOptions == null) {
117-
console.warn("Service worker not supported (or configured). Falling back to plain <script> tag.", serviceWorkerOptions);
118+
console.warn("Service worker not supported (or configured).", serviceWorkerOptions);
118119
return this._loadEntrypoint(entrypointUrl);
119120
}
120121
@@ -145,6 +146,11 @@ _flutter.loader = null;
145146
console.debug("Loading app from service worker.");
146147
return this._loadEntrypoint(entrypointUrl);
147148
}
149+
})
150+
.catch((error) => {
151+
// Some exception happened while registering/activating the service worker.
152+
console.warn("Failed to register or activate service worker:", error);
153+
return this._loadEntrypoint(entrypointUrl);
148154
});
149155
150156
// Timeout race promise
@@ -153,7 +159,7 @@ _flutter.loader = null;
153159
timeout = new Promise((resolve, _) => {
154160
setTimeout(() => {
155161
if (!this._scriptLoaded) {
156-
console.warn("Failed to load app from service worker. Falling back to plain <script> tag.");
162+
console.warn("Loading from service worker timed out after", timeoutMillis, "milliseconds.");
157163
resolve(this._loadEntrypoint(entrypointUrl));
158164
}
159165
}, timeoutMillis);

0 commit comments

Comments
 (0)