Skip to content

Commit bb066a8

Browse files
committed
fix: avoid infinite hangs on async messages
1 parent 56d9837 commit bb066a8

2 files changed

Lines changed: 88 additions & 63 deletions

File tree

src/ext/background/main.js

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,16 @@ async function nativeChecks() {
291291
}
292292

293293
/**
294-
* handles messages sent with browser.runtime.sendMessage
294+
* Handles messages sent with `browser.runtime.sendMessage`
295+
* Make sure not to return `undefined` or `rejection`, otherwise the reply may never be delivered
295296
* @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#listener}
296297
* @type {Parameters<typeof browser.runtime.onMessage.addListener>[0]}
297-
* @param {(response: any) => void} sendResponse send a response to the message
298+
* @returns {Promise<{status: "pending"|"fulfilled"|"rejected", result: any}>}
298299
*/
299-
async function handleMessage(request, sender, sendResponse) {
300-
switch (request.name) {
300+
async function handleMessage(message, sender) {
301+
/** @type {Promise} */
302+
let promise;
303+
switch (message.name) {
301304
case "REQ_USERSCRIPTS": {
302305
// get the page url from the content script that sent request
303306
const url = sender.url;
@@ -306,73 +309,75 @@ async function handleMessage(request, sender, sendResponse) {
306309
const isTop = sender.frameId === 0;
307310
// send request to swift layer to provide code for page url
308311
const message = { name: "REQ_USERSCRIPTS", url, isTop };
309-
const response = await sendNativeMessage(message);
310-
if (import.meta.env.MODE === "development") {
311-
console.debug("REQ_USERSCRIPTS", message, response);
312+
try {
313+
const response = await sendNativeMessage(message);
314+
if (import.meta.env.MODE === "development") {
315+
console.debug("REQ_USERSCRIPTS", message, response);
316+
}
317+
// if request failed, send error to content script for logging
318+
if (response.error) return response;
319+
// sort files
320+
response.files.js.sort(userscriptSort);
321+
response.files.css.sort((a, b) => Number(a.weight) < Number(b.weight));
322+
// return sorted files for injection
323+
return response;
324+
} catch (error) {
325+
console.error(error);
326+
// @ts-ignore -- ignore for now and will reconstruct this in the future.
327+
return { error };
312328
}
313-
// if request failed, send error to content script for logging
314-
if (response.error) return sendResponse(response);
315-
// sort files
316-
response.files.js.sort(userscriptSort);
317-
response.files.css.sort((a, b) => {
318-
return Number(a.weight) < Number(b.weight);
319-
});
320-
// return sorted files for injection
321-
return response;
322329
}
323330
case "API_CLOSE_TAB": {
324-
const tabId = request.tabId || sender.tab.id;
325-
return browser.tabs.remove(tabId);
331+
promise = browser.tabs.remove(message.tabId || sender.tab.id);
332+
break;
326333
}
327334
case "API_OPEN_TAB": {
328-
const props = {
329-
active: request.active,
335+
promise = browser.tabs.create({
336+
active: message.active,
330337
index: sender.tab.index + 1,
331-
url: request.url,
332-
};
333-
return browser.tabs.create(props);
338+
url: message.url,
339+
});
340+
break;
334341
}
335342
case "API_ADD_STYLE": {
336-
const tabId = sender.tab.id;
337-
/** @type {import("webextension-polyfill").ExtensionTypes.InjectDetails} */
338-
const details = { code: request.css, cssOrigin: "user" };
339-
return browser.tabs.insertCSS(tabId, details);
343+
promise = browser.tabs.insertCSS(sender.tab.id, {
344+
code: message.css,
345+
cssOrigin: "user",
346+
});
347+
break;
340348
}
341349
case "API_GET_TAB": {
342-
let tab = null;
343-
if (typeof sender.tab !== "undefined") {
350+
if (typeof sender.tab === "undefined") {
351+
const error = "unable to deliver tab due to empty tab id";
352+
return { status: "rejected", result: error };
353+
}
354+
try {
344355
const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`);
345-
try {
346-
// if tabData is null, can still parse it and return that
347-
tab = JSON.parse(tabData);
348-
} catch (error) {
349-
console.error("failed to parse tab data for getTab", error);
350-
}
351-
} else {
352-
console.error("unable to deliver tab due to empty tab id");
356+
// if tabData is null, can still parse it and return that
357+
const tabObj = JSON.parse(tabData);
358+
return { status: "fulfilled", result: tabObj };
359+
} catch (error) {
360+
console.error("failed to parse tab data for getTab", error);
361+
return { status: "rejected", result: error };
353362
}
354-
sendResponse(tab == null ? {} : tab);
355-
break;
356363
}
357364
case "API_SAVE_TAB": {
358365
if (sender.tab != null && sender.tab.id) {
359366
const key = `tab-${sender.tab.id}`;
360-
sessionStorage.setItem(key, JSON.stringify(request.tab));
361-
sendResponse({ success: true });
367+
sessionStorage.setItem(key, JSON.stringify(message.tabObj));
368+
return { status: "fulfilled", result: undefined };
362369
} else {
363-
console.error("unable to save tab, empty tab id");
364-
sendResponse({ success: false });
370+
const error = "unable to save tab, empty tab id";
371+
return { status: "rejected", result: error };
365372
}
366-
break;
367373
}
368374
case "API_SET_CLIPBOARD": {
369-
const result = setClipboard(request.clipboardData, request.type);
370-
sendResponse(result);
371-
break;
375+
const result = setClipboard(message.clipboardData, message.type);
376+
return { status: "fulfilled", result };
372377
}
373378
case "API_XHR": {
374379
// parse details and set up for XMLHttpRequest
375-
const details = request.details;
380+
const details = message.details;
376381
const method = details.method ? details.method : "GET";
377382
const user = details.user || null;
378383
const password = details.password || null;
@@ -387,7 +392,7 @@ async function handleMessage(request, sender, sendResponse) {
387392
}
388393
// establish a long-lived port connection to content script
389394
const port = browser.tabs.connect(sender.tab.id, {
390-
name: request.xhrPortName,
395+
name: message.xhrPortName,
391396
});
392397
// set up XMLHttpRequest
393398
const xhr = new XMLHttpRequest();
@@ -397,7 +402,7 @@ async function handleMessage(request, sender, sendResponse) {
397402
xhr.overrideMimeType(details.overrideMimeType);
398403
}
399404
// add required listeners and send result back to the content script
400-
for (const e of request.events) {
405+
for (const e of message.events) {
401406
if (!details[e]) continue;
402407
xhr[e] = async (event) => {
403408
// can not send xhr through postMessage
@@ -487,6 +492,13 @@ async function handleMessage(request, sender, sendResponse) {
487492
break;
488493
}
489494
}
495+
try {
496+
const result = await promise;
497+
return { status: "fulfilled", result };
498+
} catch (error) {
499+
console.error(message, sender, error);
500+
return { status: "rejected", result: error };
501+
}
490502
}
491503
browser.runtime.onInstalled.addListener(async () => {
492504
await nativeChecks();

src/ext/content-scripts/api.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,43 +52,56 @@ async function listValues() {
5252
return keys;
5353
}
5454

55+
async function sendMessageProxy(message) {
56+
try {
57+
/** @type {{status: "fulfilled"|"rejected", result: any}} */
58+
const response = await browser.runtime.sendMessage(message);
59+
if (response.status === "fulfilled") {
60+
return response.result;
61+
} else {
62+
return Promise.reject(response.result);
63+
}
64+
} catch (error) {
65+
console.error(error);
66+
return Promise.reject(error);
67+
}
68+
}
69+
5570
async function openInTab(url, openInBackground = false) {
5671
try {
5772
new URL(url);
5873
} catch (error) {
5974
return Promise.reject(error);
6075
}
61-
return browser.runtime.sendMessage({
76+
return sendMessageProxy({
6277
name: "API_OPEN_TAB",
6378
url,
6479
active: !openInBackground,
6580
});
6681
}
6782

68-
async function getTab() {
69-
return browser.runtime.sendMessage({ name: "API_GET_TAB" });
83+
async function closeTab(tabId) {
84+
return sendMessageProxy({ name: "API_CLOSE_TAB", tabId });
7085
}
7186

72-
async function saveTab(tab) {
73-
if (tab == null) {
74-
return Promise.reject(new Error("saveTab invalid arg"));
75-
}
76-
return browser.runtime.sendMessage({ name: "API_SAVE_TAB", tab });
87+
async function getTab() {
88+
return sendMessageProxy({ name: "API_GET_TAB" });
7789
}
7890

79-
async function closeTab(tabId) {
80-
return browser.runtime.sendMessage({ name: "API_CLOSE_TAB", tabId });
91+
async function saveTab(tabObj) {
92+
if (tabObj == null) return Promise.reject(new Error("saveTab invalid arg"));
93+
return sendMessageProxy({ name: "API_SAVE_TAB", tabObj });
8194
}
8295

8396
async function addStyle(css) {
8497
if (typeof css !== "string" || !css.length) {
8598
return Promise.reject(new Error("addStyle invalid css arg"));
8699
}
87-
return browser.runtime.sendMessage({ name: "API_ADD_STYLE", css });
100+
return sendMessageProxy({ name: "API_ADD_STYLE", css });
88101
}
89102

90103
async function setClipboard(clipboardData, type) {
91-
return browser.runtime.sendMessage({
104+
return sendMessageProxy({
92105
name: "API_SET_CLIPBOARD",
93106
clipboardData,
94107
type,
@@ -174,7 +187,7 @@ function xhr(details) {
174187
xhrPortName,
175188
events,
176189
};
177-
browser.runtime.sendMessage(message);
190+
sendMessageProxy(message);
178191
return response;
179192
}
180193

0 commit comments

Comments
 (0)