DEV Community

Cover image for Implementing `onicecandidateerror` in Firefox — A Missing Piece of the WebRTC Spec
Anshul Malik
Anshul Malik

Posted on

Implementing `onicecandidateerror` in Firefox — A Missing Piece of the WebRTC Spec

If you've ever debugged a WebRTC connection failure in Firefox and wished you had more information about why ICE candidate gathering failed, you're not alone. For years, Firefox was missing a spec-mandated event that Chrome had already implemented: onicecandidateerror (also known as RTCPeerConnectionIceErrorEvent).

I recently landed a patch that finally brings this to Firefox. Here's what the bug was, what the fix involved, and why it matters.


What is onicecandidateerror?

When a WebRTC peer connection tries to gather ICE candidates, it contacts your configured STUN and TURN servers. If one of those servers is unreachable, misconfigured, or returns an error, the browser needs a way to tell you about it.

The W3C WebRTC spec defines onicecandidateerror exactly for this purpose. It fires an RTCPeerConnectionIceErrorEvent with:

  • url — which STUN/TURN server failed
  • errorCode — the STUN error code (e.g. 401 Unauthorized, 701 for custom errors)
  • errorText — human-readable error description
  • address / port — the local address used when the error occurred (may be null for privacy)

Without this event, if your TURN server credentials are wrong or a STUN server is unreachable, Firefox would silently fail to gather certain candidates. You'd see ICE connectivity failures with no useful diagnostic signal from the browser.

Chrome has had this implemented for years. Firefox hadn't — until now.


What the Fix Involved

The patch touches several layers of Firefox's architecture, which is a good illustration of how deep WebRTC signaling goes in a browser engine.

1. A new data structure — IceCandidateErrorInfo

The first thing needed was a struct to carry the error information from the ICE layer up through the stack:

struct IceCandidateErrorInfo {
  std::string mAddress;
  uint16_t mPort = 0;
  std::string mUrl;
  uint16_t mErrorCode = 0;
  std::string mErrorText;
};
Enter fullscreen mode Exit fullscreen mode

This lives in CandidateInfo.h alongside the existing CandidateInfo struct used for successful candidates.

2. IPC plumbing — PMediaTransport.ipdl

Firefox's WebRTC transport layer runs in a separate process. Communication between processes happens via IPC (Inter-Process Communication) using Mozilla's IPDL protocol definition language.

A new async message was added to carry the error from the transport process to the main process:

async OnCandidateError(IceCandidateErrorInfo errorInfo);
Enter fullscreen mode Exit fullscreen mode

The IceCandidateErrorInfo struct also had to be registered for IPC serialization so it could be passed across process boundaries:

DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::IceCandidateErrorInfo,
                                  mAddress, mPort, mUrl, mErrorCode, mErrorText)
Enter fullscreen mode Exit fullscreen mode

3. Event dispatch — PeerConnection.sys.mjs

On the JavaScript side, a new handler was wired up to receive the error and fire the DOM event:

this.makeGetterSetterEH("onicecandidateerror");
Enter fullscreen mode Exit fullscreen mode

And the actual event dispatch:

onIceCandidateError(address, port, url, errorCode, errorText) {
  let win = this._dompc._win;
  this.dispatchEvent(
    new win.RTCPeerConnectionIceErrorEvent("icecandidateerror", {
      address: address !== "" ? address : null,
      port: port !== 0 ? port : null,
      url,
      errorCode,
      errorText,
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

Note the null handling for address and port — the spec requires these to be null when not available, which is important for privacy reasons (you don't always want to expose the local address).

4. Tests

Two tests were added/updated:

Synthetic event test — verifies that RTCPeerConnectionIceErrorEvent can be constructed and dispatched correctly:

RTCPeerConnectionIceErrorEvent: {
  create(aName, aProps) {
    return new RTCPeerConnectionIceErrorEvent(
      aName,
      Object.assign({errorCode: 701}, aProps)
    );
  },
},
Enter fullscreen mode Exit fullscreen mode

Web platform test — a more realistic end-to-end test that configures a peer connection with intentionally invalid STUN/TURN servers and verifies that icecandidateerror fires for each one:

const iceServers = [
  {urls: "turn:turn1.invalid:3478", username: "u", credential: "p"},
  {urls: "turn:turn2.invalid:3478?transport=tcp", username: "u", credential: "p"},
  {urls: "turns:turn3.invalid:5349", username: "u", credential: "p"},
  {urls: "stun:stun1.invalid:3478"},
];

// ... verify icecandidateerror fires for each server
for (const event of errors) {
  assert_true(event instanceof RTCPeerConnectionIceErrorEvent);
  assert_true(expectedUrls.has(event.url));
  assert_true(event.errorCode >= 300 && event.errorCode <= 799);
}
Enter fullscreen mode Exit fullscreen mode

This test also removed a bunch of previously expected failures from Firefox's web-platform-test results — those expected: FAIL entries for RTCPeerConnectionIceErrorEvent are now gone.


Why This Matters

If you build anything with WebRTC — video conferencing, real-time collaboration, telehealth — you've probably hit ICE failures that were hard to debug in Firefox. Now you can do this:

const pc = new RTCPeerConnection({ iceServers: myServers });

pc.onicecandidateerror = (event) => {
  console.error(`ICE error from ${event.url}: [${event.errorCode}] ${event.errorText}`);
};
Enter fullscreen mode Exit fullscreen mode

This works in Chrome, Safari, and now Firefox too. One less cross-browser gap for WebRTC developers to work around.


The Bigger Picture

This patch is part of my ongoing work on Firefox's WebRTC signaling stack — ICE, SDP/JSEP, STUN/TURN — as an independent open-source contributor. There's still a lot of ground to cover to bring Firefox fully in line with the spec and improve cross-browser interoperability.

If this kind of work is useful to you or your product, consider sponsoring me on GitHub. It helps me dedicate more time to these fixes.

You can follow my patches on Bugzilla.


Bug reference: Bug 1561441

Top comments (0)