Skip to content

Commit eec4502

Browse files
authored
Merge pull request #957 from clairernovotny/service-worker-update
Enhance service worker error handling and caching logic
2 parents c4964ca + 5a1aeda commit eec4502

File tree

3 files changed

+125
-38
lines changed

3 files changed

+125
-38
lines changed

doc/features-environment-variables.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The bootstrapper provides a set of environment variables that reflect the config
2626
- `UNO_BOOTSTRAP_MONO_RUNTIME_MODE`, which specifies the runtime mode configuration (see above for valid values)
2727
- `UNO_BOOTSTRAP_LINKER_ENABLED`, which is set to `True` if the linker was enabled, otherwise `False`
2828
- `UNO_BOOTSTRAP_DEBUGGER_ENABLED`, which is set to `True` if the debugging support was enabled, otherwise `False`
29+
- `UNO_BOOTSTRAP_FETCH_RETRIES`, which specifies how many ties `fetch` should retry on a failed request. Defaults to `1`
2930
- `UNO_BOOTSTRAP_MONO_RUNTIME_CONFIGURATION`, which provides the mono runtime configuration, which can be can either be `release` or `debug`.
3031
- `UNO_BOOTSTRAP_MONO_RUNTIME_FEATURES`, which provides a list of comma separated feature enabled in the runtime (e.g. `threads`)
3132
- `UNO_BOOTSTRAP_MONO_PROFILED_AOT`, which specifies if the package was built using a PG-AOT profile.

src/Uno.Wasm.Bootstrap/Embedded/service-worker.js

Lines changed: 121 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { config as unoConfig } from "$(REMOTE_WEBAPP_PATH)$(REMOTE_BASE_PATH)/uno-config.js";
22

3-
43
if (unoConfig.environmentVariables["UNO_BOOTSTRAP_DEBUGGER_ENABLED"] !== "True") {
54
console.debug("[ServiceWorker] Initializing");
65
let uno_enable_tracing = unoConfig.uno_enable_tracing;
76

7+
// Get the number of fetch retries from environment variables or default to 1
8+
const fetchRetries = parseInt(unoConfig.environmentVariables["UNO_BOOTSTRAP_FETCH_RETRIES"] || "1");
9+
810
self.addEventListener('install', function (e) {
911
console.debug('[ServiceWorker] Installing offline worker');
1012
e.waitUntil(
@@ -15,65 +17,146 @@ if (unoConfig.environmentVariables["UNO_BOOTSTRAP_DEBUGGER_ENABLED"] !== "True")
1517
// worker to fail installing.
1618
for (var i = 0; i < unoConfig.offline_files.length; i++) {
1719
try {
20+
const currentFile = unoConfig.offline_files[i];
1821
if (uno_enable_tracing) {
19-
console.debug(`[ServiceWorker] cache ${key}`);
22+
console.debug(`[ServiceWorker] caching ${currentFile}`);
2023
}
2124

22-
await cache.add(unoConfig.offline_files[i]);
25+
await cache.add(currentFile);
2326
}
2427
catch (e) {
25-
console.debug(`[ServiceWorker] Failed to fetch ${unoConfig.offline_files[i]}`);
28+
console.debug(`[ServiceWorker] Failed to fetch ${unoConfig.offline_files[i]}: ${e.message}`);
2629
}
2730
}
2831

2932
// Add the runtime's own files to the cache. We cannot use the
3033
// existing cached content from the runtime as the keys contain a
3134
// hash we cannot reliably compute.
32-
var c = await fetch("$(REMOTE_WEBAPP_PATH)_framework/blazor.boot.json");
33-
const monoConfigResources = (await c.json()).resources;
34-
35-
var entries = {
36-
...(monoConfigResources.coreAssembly || {})
37-
, ...(monoConfigResources.assembly || {})
38-
, ...(monoConfigResources.lazyAssembly || {})
39-
, ...(monoConfigResources.jsModuleWorker || {})
40-
, ...(monoConfigResources.jsModuleGlobalization || {})
41-
, ...(monoConfigResources.jsModuleNative || {})
42-
, ...(monoConfigResources.jsModuleRuntime || {})
43-
, ...(monoConfigResources.wasmNative || {})
44-
, ...(monoConfigResources.icu || {})
45-
, ...(monoConfigResources.coreAssembly || {})
46-
};
47-
48-
for (var key in entries) {
49-
var uri = `$(REMOTE_WEBAPP_PATH)_framework/${key}`;
50-
51-
if (uno_enable_tracing) {
52-
console.debug(`[ServiceWorker] cache ${uri}`);
35+
try {
36+
var c = await fetch("$(REMOTE_WEBAPP_PATH)_framework/blazor.boot.json");
37+
// Response validation to catch HTTP errors early
38+
// This prevents trying to parse invalid JSON from error responses
39+
if (!c.ok) {
40+
throw new Error(`Failed to fetch blazor.boot.json: ${c.status} ${c.statusText}`);
5341
}
5442

55-
await cache.add(uri);
43+
const bootJson = await c.json();
44+
const monoConfigResources = bootJson.resources || {};
45+
46+
var entries = {
47+
...(monoConfigResources.coreAssembly || {}),
48+
...(monoConfigResources.assembly || {}),
49+
...(monoConfigResources.lazyAssembly || {}),
50+
...(monoConfigResources.jsModuleWorker || {}),
51+
...(monoConfigResources.jsModuleGlobalization || {}),
52+
...(monoConfigResources.jsModuleNative || {}),
53+
...(monoConfigResources.jsModuleRuntime || {}),
54+
...(monoConfigResources.wasmNative || {}),
55+
...(monoConfigResources.icu || {})
56+
};
57+
58+
for (var key in entries) {
59+
var uri = `$(REMOTE_WEBAPP_PATH)_framework/${key}`;
60+
61+
try {
62+
if (uno_enable_tracing) {
63+
console.debug(`[ServiceWorker] cache ${uri}`);
64+
}
65+
66+
await cache.add(uri);
67+
} catch (e) {
68+
console.error(`[ServiceWorker] Failed to cache ${uri}:`, e.message);
69+
}
70+
}
71+
} catch (e) {
72+
// Centralized error handling for the entire boot.json processing
73+
console.error('[ServiceWorker] Error processing blazor.boot.json:', e.message);
5674
}
5775
})
5876
);
5977
});
6078

79+
// Cache cleanup logic to prevent storage bloat
80+
// This removes any old caches that might have been created by previous
81+
// versions of the service worker, helping prevent storage quota issues
6182
self.addEventListener('activate', event => {
62-
event.waitUntil(self.clients.claim());
83+
event.waitUntil(
84+
caches.keys().then(function (cacheNames) {
85+
return Promise.all(
86+
cacheNames.filter(function (cacheName) {
87+
return cacheName !== '$(CACHE_KEY)';
88+
}).map(function (cacheName) {
89+
console.debug('[ServiceWorker] Deleting old cache:', cacheName);
90+
return caches.delete(cacheName);
91+
})
92+
);
93+
}).then(function () {
94+
return self.clients.claim();
95+
})
96+
);
6397
});
6498

6599
self.addEventListener('fetch', event => {
66-
event.respondWith(async function () {
67-
try {
68-
// Network first mode to get fresh content every time, then fallback to
69-
// cache content if needed.
70-
return await fetch(event.request);
71-
} catch (err) {
72-
return caches.match(event.request).then(response => {
73-
return response || fetch(event.request);
74-
});
75-
}
76-
}());
100+
event.respondWith(
101+
(async function () {
102+
// FIXED: Critical fix for "already used" Request objects #956
103+
// Request objects can only be used once in a fetch operation
104+
// Cloning the request allows for reuse in fallback scenarios
105+
const requestClone = event.request.clone();
106+
107+
try {
108+
// Network first mode to get fresh content every time, then fallback to
109+
// cache content if needed.
110+
return await fetch(requestClone);
111+
} catch (err) {
112+
// Logging to track network failures
113+
console.debug(`[ServiceWorker] Network fetch failed, falling back to cache for: ${requestClone.url}`);
114+
115+
const cachedResponse = await caches.match(event.request);
116+
if (cachedResponse) {
117+
return cachedResponse;
118+
}
119+
120+
// Add retry mechanism - attempt to fetch again if retries are configured
121+
if (fetchRetries > 0) {
122+
console.debug(`[ServiceWorker] Resource not in cache, attempting ${fetchRetries} network retries for: ${requestClone.url}`);
123+
124+
// Try multiple fetch attempts with exponential backoff
125+
for (let retryCount = 0; retryCount < fetchRetries; retryCount++) {
126+
try {
127+
// Exponential backoff between retries (500ms, 1s, 2s, etc.)
128+
const retryDelay = Math.pow(2, retryCount) * 500;
129+
await new Promise(resolve => setTimeout(resolve, retryDelay));
130+
131+
if (uno_enable_tracing) {
132+
console.debug(`[ServiceWorker] Retry attempt ${retryCount + 1}/${fetchRetries} for: ${requestClone.url}`);
133+
}
134+
135+
// Need a fresh request clone for each retry
136+
return await fetch(event.request.clone());
137+
} catch (retryErr) {
138+
if (uno_enable_tracing) {
139+
console.debug(`[ServiceWorker] Retry ${retryCount + 1} failed: ${retryErr.message}`);
140+
}
141+
// Continue to next retry attempt
142+
}
143+
}
144+
}
145+
146+
// Graceful error handling with a proper HTTP response
147+
// Rather than letting the fetch fail with a generic error,
148+
// we return a controlled 503 Service Unavailable response
149+
console.error(`[ServiceWorker] Resource not available in cache or network after ${fetchRetries} retries: ${requestClone.url}`);
150+
return new Response('Network error occurred, and resource was not found in cache.', {
151+
status: 503,
152+
statusText: 'Service Unavailable',
153+
headers: new Headers({
154+
'Content-Type': 'text/plain'
155+
})
156+
});
157+
}
158+
})()
159+
);
77160
});
78161
}
79162
else {

src/Uno.Wasm.Bootstrap/ShellTask.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ public partial class ShellTask_v0 : Task
125125

126126
public bool GenerateAOTProfile { get; set; }
127127

128+
public string FetchRetries { get; set; } = "1";
129+
128130
public bool EnableThreads { get; set; }
129131

130132
public ITaskItem[]? ReferencePath { get; set; }
@@ -590,6 +592,7 @@ private void GenerateConfig()
590592
AddEnvironmentVariable("UNO_BOOTSTRAP_MONO_PROFILED_AOT", isProfiledAOT.ToString());
591593
AddEnvironmentVariable("UNO_BOOTSTRAP_LINKER_ENABLED", (PublishTrimmed && RunILLink).ToString());
592594
AddEnvironmentVariable("UNO_BOOTSTRAP_DEBUGGER_ENABLED", (!Optimize).ToString());
595+
AddEnvironmentVariable("UNO_BOOTSTRAP_FETCH_RETRIES", FetchRetries);
593596
AddEnvironmentVariable("UNO_BOOTSTRAP_MONO_RUNTIME_CONFIGURATION", "Release");
594597
AddEnvironmentVariable("UNO_BOOTSTRAP_MONO_RUNTIME_FEATURES", BuildRuntimeFeatures());
595598
AddEnvironmentVariable("UNO_BOOTSTRAP_APP_BASE", PackageAssetsFolder);

0 commit comments

Comments
 (0)