onHeadersReceived: call browserAction.set*({tabId: …})

I’m developing an extension that, among other things, must set browserAction subject to the following constraints:

  • manipulations are done at the tab-level (i.e. not globally)
  • they are done based on main_frame requests, in a 1:1 way
  • they require information only available in onHeadersReceived.

However, calling any browserAction.set* function from within an onHeadersReceived listener only applies for a split-instant; the icon is, apparently, reset by the browser at the end of the request.

I’ve “monkey-patched” it for now by applying the browserAction details within a setTimeout of 250ms (which seems to yield precisely the desired behavior); but this is a very kludgy and wrong workaround.

So, I ask: what is the “proper” way to call browserAction.set* with a tabId from [code logically originating within] an onHeadersReceived listener? I assume it’d have something to do with something like, perhaps, setting up a Promise to resolve on the request’s completion, but I’m stumped from reading the documentation.

Browser action state is window specific, so you can’t use tabIDs with it. If the contents of your action are also page specific, consider using a page action, which has a tab specific state (but no badge).

This seems to me like you have some other event of yours triggering whenever the request is done that resets the icon, I have never encountered the browser resetting the icon, unless the extension is reloaded and thus reset to the default icon.

I’m sorry, but this is incorrect:

Parameters:

  • details
    object . An object containing either imageData or path properties, and optionally a tabId property.
    • tabId (Optional)
      integer . Sets the icon only for the given tab. The icon is reset when the user navigates this tab to a new page.

[ETA: ditto the above for all browserAction.set* functions.]

As I stated in my original post, setting this delay makes it work seemingly precisely as intended (I even bolded the word “precisely” for emphasis):

I have done a modicum of testing in all reasonable cases (multiple tabs, multiple windows, different icons in each, etc.) and found that a Firefox Add-on [as per the MDN docs] certainly can set a per-tab state for its BrowserAction, which is distinct from the same’s “Global” state.

My question is: how to deterministically manipulate this state from a function (onHeadersReceived) only available so early in the WebRequest pipeline.

Huh, I must’ve totally forgotten about the tab based behavior, excuse me for that and thank you for the correction.

The solution is likely to have a cache of states that you apply once the webNavigation onCompleted or onDOMLoaded comes in (not sure if onDOMLoaded is guaranteed to be before the browserAction state reset, not familiar enough with it). You could even abstract waiting for this event away with a promise and use an async function as listener in onHeadersReceived, so the logic would still all be in that listener.

Can you create a little queue of icon changes keyed to the tabId and consult/apply it on:

2 Likes

apply it on tabs.onUpdated

@jscher2000 that looks like it’ll do; thanks for the suggestion.

I’ll try it out this evening… technically, any event which occurs strictly after the tab’s browserAction gets reset will do, though ones which occur very close will appear indistinguishable at first. :confused: It would definitely be nice to figure out which ones properly/formally won’t “miss the bus” on not-being-overwritten.


Also, this is probably worthy of its own thread, but I’ve got the following function in my code… is there any way to do the following atomically?

updateBrowserAction(
    this.tabId,
    {
      Icon: {path: 'images/secure.png'},
      Title: {title: `the sender has been validated.\nTrustLevel: ${n}`},
      BadgeText: {text: n},
      BadgeBackgroundColor: {color: 'LimeGreen'}
    }
);

(e.g. there’s a bit of lag on the PC, and one of the properties’ applications is delayed and applied at an…inappropriate…time, perhaps—in adversarially-contrived cases—even overwriting the subsequent, current display [e.g. “insecure”] due to being delayed)

the function is defined below

function updateBrowserAction(tabId,browserActionSpec){
	//TODO: why does Firefox not give us an atomic version of this function??
	let cmdDefaults={};
	if(tabId!=-1) cmdDefaults.tabId=tabId;
	for(let prop in browserActionSpec){
		//consider parallelizing this loop to mitigate possible race condition meanwhile
		let cmd=Object.assign(new Object(),
		    cmdDefaults,
		    browserActionSpec[prop]);
		browser.browserAction['set'+prop](cmd);
	}
}

I ended up creating a global const object

  • within webRequest.onHeadersReceived, I store the planned browserAction specification at queuedBrowserActionSpec[tabId]
  • within tabs.onUpdated (if its update status=='complete'):
    • let browserActionSpec = queuedBrowserActionSpec[tabId]
    • delete queuedBrowserActionSpec[tabId]
    • I apply browserActionSpec
  • I also added in a copy of the code that just deletes it into tabs.onRemoved

This seems to work OK for now.

1 Like