This document captures requirements to evolve protocol handlers as available on the web. The goal is to make it possible to extend the set of protocols that are supported while maintaining a high level of security and trust for users.

Handlers Beyond Handoff

Protocol handlers today are focused on single-shot handoff. When activating a mailto: link, the browser will either hand it off to the OS for a native application to handle, or will pass it to a (top-level) URL that will receive it as a parameter and process it. In either case, the interaction is not incorporated into the current context and no further interaction with the protocol is possible. In effect, this is more a scheme dispatcher than a comprehensive handler for a given protocol.

Today's on-ramp for new protocols to become used on the web beyond a simple handoff however is too steep. While there is interest from implementers in loosening handler registration, that does not extend to the ability to fully integrate new protocols into the web stack such that they can be the source of top-level documents, iframes, fetch, images, etc.

This document captures requirements for browser extensions to be able to register as full-fledged (or at least fuller-fledged) programmatic protocol handlers. For instance, using this approach an extension could implement support for IPFS directly within any browser context.

Integration

One goal is that registering a protocol handler in different ways and at different levels should map to a unified underlying model to the extent possible, so as to ensure that a single code path controls protocol handling.

With progress towards a IANA+blocklist approach as advocated in whatwg/html#9158, there is no need to limit registration to web+- or ext+-prefixed schemes.

The proposed integration method for new protocol handling is to operate a Service Worker for that protocol that can respond to FetchEvents. The assumption is that the browser will address any and all schemes with HTTP semantics, and it is up to the handler to figure out the appropriate mapping, knowing that there will likely be impedance mismatch.

The current proposal only exposes this capability to extensions. In that context, registering a Service Worker for handling by an extension is done using the protocol_handlers field. We suggest adding a serviceWorker field that points to a JS resource in the extension implementing a Service Worker. The serviceWorker field takes precedence over the uriTemplate field (but having both is not an error, this allows for an upgrade path). (See notes from lidel tracking the idea.)

Note that custom protocol handlers of this kind need to be integrated at the same architectural position as the browser's built-in protocol handling. Notably, they kick in after extension points to block or modify network requests have been processed. If, for instance, an ad blocker has a denylist that includes ipfs: URLs, those will be processed before the IPFS protocol handler is invoked.

In the case of handoff protocol handlers, user agents can simply prompt the user to pick which handler they wish to use after activating a link with a custom protocol. With programmatic handlers that can be called upon to load a subresource, that is not a desirable behavior. In cases where a new handler is being registered for a scheme that there is already a handler for, the user agent is encouraged to pick which one will be used. User agents may simply use the latest handler to be registered, or may prompt the user to select the one they wish to use by default. User agents should avoid having both programmatic handlers and handoff handlers registered for the same scheme as that can create a confusing experience if clicking on a custom protocol link offers to choose between those two but loading a subresource only selects the programmatic one. In such cases, systematically using the the programmatic handler is likely to be the most user-friendly option.

A manifest with a Service Worker handling the protocol looks like this:

        {
          "manifest_version": 2,
          "name": "InterPlanetary Wonders",
          "icons": {
            "48": "planets.png",
            "96": "planets@2x.png"
          },
          "version": "17.0.1",
          "protocol_handlers": [
            {
              "protocol": "ipfs",
              "name": "IPFS Makes Your Browser Fresh",
              "serviceWorker": "js/gateway-loader.js"
            }
          ]
        }
      

And the Service Worker is straightforward:

        self.addEventListener("fetch", (ev) => {
          ev.respondWith(
            (async () => {
              const url = new URL(ev.request.url);
              const gatewayURL = [
                'https://ipfs.io/ipfs/',
                url.hostname,
                url.pathname || '/',
                url.search || '',
                url.hash || '',
              ].join('');
              const { method, headers, body, mode, credentials, cache, redirect,
                referrer, referrerPolicy, integrity, signal } = ev.request;
              return fetch(new Request(
                gatewayURL,
                { method, headers, body, mode, credentials, cache, redirect,
                  referrer, referrerPolicy, integrity, signal }
              ));
            })(),
          );
        });
      

Availability to Web Apps

In addition to extensions, this level of protocol handling could also be made available to PWAs. In terms of design and code paths, the same protocol_handlers entry could be used in the web app manifests as we are recommending for extensions. Should that be supported, we don't anticipate the design of protocol handlers to need to be different.

Extensions are the safest way to support this extension point, and it seems wise to limit support to that pathway at least at first. However, it is good to note that the support could be extended to PWAs without rearchitecting the system and without any change in syntax.

Integration Considerations

In order to plug a protocol as an extension to the browser's capabilities, we need to address more than simply handing a URL off to an API that returns a stream. While it is possible to expose the many options involved through an API, it is easy enough to shoot yourself in the foot and has enough potential security issues that our preference goes to selecting a strict default behavior instead.

URL Processing

URLs integrate deeply into the web stack and specific schemes can have a host (pun intended) of different behaviors.

Origins

In order to integrate into the stack, a new scheme needs to map its URL-space onto origins. However, not all schemes have host and port components.

Proposal. URLs are parsed as per [[url]]. Because the browser cannot guess the default port for that scheme, the API is expected to provide it, if it is meaningful.

Credentials

URLs may have credentials components, and the basic URL parser will parse them if they are present. These however can be problematic, what with embedding credentials in plain text and all.

Proposal. The browser parses them but removes them from the URL before calling the Service Worker.

Canonicalization

In HTTP URLs, paths get canonicalized (e.g. .. path segments are removed) but that is not the case with all schemes.

Proposal. Developer expectations are that these canonicalized are resolved and many resources simply assume it. We should canonicalize automatically.

Writability

Many protocols aren't just read-only but also allow for writing. The browser needs to know what to do to handle [[fetch]] verbs beyond GET and HEAD as well as form submissions.

Proposal. One simpler option is to start with read-only; anything else produces a 405 error. A writable iteration could involve using FetchEvent to process arbitrary HTTP requests and gateway them to the underlying protocol. Alternatively, we may go straight to the latter if there is no specific concern with the approach.

Secure

We need to determine if schemes are secure (support more powerful APIs, are allowed as mixed content).

Proposal. Extension-supported schemes are considered secure. Extensions are expected to make sure that they do not map to insecure protocols.

CORS

Is the scheme subject to CORS gating?

Proposal. Yes. It is easy enough for an extension to handle CORS handshakes itself according to its own preference.

Referrer

Does this scheme get sent as a referrer for loaded resources or navigations?

Proposal. No. The strict CSP should prevent the loading of resources from content loaded in an extension-supported scheme, which would make a referrer moot. For navigation, the potential tracking risk is not worth the value.

Top-level frame

Is this scheme only for resources loaded from another context or can it be loaded at the top level?

Proposal. It can be loaded at the top level.

Local Storage

Does the scheme support setting cookies, using local storage, etc. and if so according to which rules.

Proposal. Extension-supported schemes support local storage and cookies. Implementations are expected to apply partitioning based on the origin (as described above) as they would with an HTTP origin.

CSP

We need to integrate into the CSP model for security to work properly.

Proposal. Default to a very strict CSP and don't allow the extension to override it. We can loosen things later if need be.

Sandboxing

Two relevant aspects apply with respect to sandboxing:

Proposal. The sandbox attribute applies as it does in other contexts, ther is nothing special about content from custom protocols given that they can be mapped from fetch. These scheme do count as custom scheme even if they are implemented closer to the metal simply because that list is fixed to all non-fetch schemes.

Other questions

More questions need to be answered (some from Eric Lawrence's list):

We also need to look more closely at the details of what browsers do. For instance, Gecko requires that protocols implement nsIProtocolHandler, which returns a nsIChannel. That doesn't cover all the integration, though, for instance CSP would happen elsewhere.

Acknowledgements

Many thanks to the following people, in alphabetical order, for their invaluable input: Brian Kardell, Dave Justice, Dietrich Ayala, Fabrice Desré, Javier Fernández, and lidel.