The Process Boundary

On the message path, the input and the view run on the child side of the Urlbar JSWindowActor pair, and the parent controller, the providers and the muxers run on the parent side. Everything that passes between them is a message, so what arrives is a structured clone of what was sent. This page describes what crosses that boundary and what provider and view code has to account for.

On the direct path both sides share the same objects, so code that breaks these rules usually still works there. To catch it, run the tests with browser.urlbar.ipc.chromeMessagePassing set to true, which puts the toolbar’s address and search bars on the message path too.

Results

A result crosses as the plain object that UrlbarResult.toWire() returns, and UrlbarResult.fromWire() turns it back into a result. Any property set on a result other than id, rowIndex, commands and isSERP is lost.

Every result a provider adds gets an id from the providers manager. Results that the view builds itself have none. When a result comes back to the parent, for example on a selection or an engagement, fromWire() looks the id up among the results of the parent’s last query and returns the original object, so a provider sees the result it created. If the result isn’t there, because the query has moved on or the result came from a one-off heuristic query such as paste and go, fromWire() builds a new result from the wire form. That copy is a different object from the one the provider created, its payload is a structured clone of the original, as described below, and it skips payload validation.

Payloads

The UrlbarResult constructor makes a shallow copy of the payload it is given. This copy reads each getter once and keeps only own enumerable properties that aren’t null or undefined, on both paths. On the message path the payload is then structured-cloned, so a function in it makes the message fail, and a class instance arrives as a plain object without its prototype or methods. For example, a Firefox Suggest result from the Rust backend keeps the Rust component’s Suggestion object in its payload as suggestionObject. The view gets it as a plain object, so code that needs the real object, such as a dismissal, has to run in the parent against the original result.

Payload validation needs system modules, so it is skipped in a content realm. Provider results are validated in the parent before they cross, but a result that a view in a content process builds itself is not validated. An invalid payload in such a result throws when the view runs in the parent, regardless of browser.urlbar.ipc.chromeMessagePassing, but goes unnoticed in a content process.

View data

Everything the view needs from the provider is computed in the parent and put into the UrlbarResult, so the view can read it synchronously without calling back across the boundary.

A provider that needs to change a row afterwards has to add a new result in a later query. For the result menu only, view.updateResultMenuCommands() replaces the commands of a row that is already shown.

DOM nodes and events

DOM nodes and events never cross. The engagement data sent to the parent drops details.element and details.event, so a provider’s onEngagement() sees null for both on the message path. onBeforeSelection() receives no element there either.

Loads

When the user picks a result, the content side asks the parent to load it. The load parameters it sends can include principals such as triggeringPrincipal. Post data travels as a string, and the parent turns it back into a stream. Neither a <browser> nor the chrome document can be sent, so the parent adds them itself: the target <browser> for a load into the current tab, and the chrome document for a load elsewhere.

The parent decides which <browser> a load targets. An input in a content process always targets its own tab, whatever browserId it sends. An input in the chrome window identifies the browser by its browserId, which the parent resolves with BrowsingContext.getCurrentTopByBrowserId(), and gets the selected browser when it sends none.

An nsIURI doesn’t survive a structured clone, and neither does an nsIURIFixupInfo. URI fixup results reach the content side as plain values instead: URIFixupPrimitives holds the keywordAsSent and the preferredURIDisplaySpec of a fixup, and the fallback navigation on Enter returns the URL to load as a string, with its post data and keywordAsSent.

Provider hooks in the parent

Provider hooks such as onEngagement(), onImpression(), onAbandonment() and onSelection() always run in the parent. On the message path they receive the results that fromWire() resolved, and the view sends onBeforeSelection() and onSelection() as messages without waiting for the provider.

The controller passed to a provider is the parent controller. On the message path, its input and view are stand-ins built by UrlbarParent. They offer only the methods listed in UrlbarShared.INVOKABLE_CONTENT_ACTIONS, such as:

  • input: search(), setValue() and startQuery()

  • view: acknowledgeFeedback(), clearL10nCache(), clearTopSitesCache(), close() and updateResultMenuCommands()

Every other property reads undefined. A call sends a message and returns nothing, so its effect on the input or the view happens after the hook has returned. If the page with the input has already gone away, the call is dropped silently. Calling another method from the parent requires adding it to INVOKABLE_CONTENT_ACTIONS, and its arguments have to be structured-clonable. A result passed as an argument arrives without its private fields, so pass the result’s id instead, as acknowledgeFeedback() and updateResultMenuCommands() do.

A provider that needs the chrome window reads controller.browserWindow, which the parent resolves from the actor. controller.input.window doesn’t exist on the message path. The results that were visible at engagement time come with the engagement data rather than from the view, since the parent’s view has none.