Closed Bug 2018724 Opened 4 months ago Closed 2 months ago

Firefox fires both error and close events on websocket connection loss

Categories

(Core :: DOM: Networking, defect, P4)

defect

Tracking

()

RESOLVED WONTFIX

People

(Reporter: saschanaz, Unassigned)

References

Details

(Whiteboard: [necko-triaged])

Copypasting bug 1944288.

  1. Set up a local Deno server with the following script (sends websocket message every second):
Deno.serve((req) => {
  if (req.headers.get("upgrade") != "websocket") {
    return new Response(`
      <!DOCTYPE html>
      <div id="log"></div>
      <script>
        const onmessage = () => {
          log.prepend(document.createElement("br"))
          log.prepend(Date.now())
        }
        const setup = () => {
          log.prepend(document.createElement("br"))
          log.prepend(Date.now() + " connecting websocket");
          socket = new WebSocket("/");
          socket.onmessage = onmessage;
          socket.onclose = setup;
          socket.onerror = setup;
        }
        setup();
      </script>
    `, {
      headers: {
        "content-type": "text/html"
      }
    });
  }

  const userAgent = req.headers.get("user-agent")

  const { socket, response } = Deno.upgradeWebSocket(req);

  socket.addEventListener("open", () => {
    console.log(`a client connected! ${userAgent}`);
  });

  const handle = setInterval(() => {
    socket.send("ping");
  }, 1000)

  socket.addEventListener("close", () => {
    console.log(`a client disconnected! ${userAgent}`)
    clearTimeout(handle);
  });

  return response;
});
  1. Access the page on Firefox for Android and Chrome for Android
  2. Either turn off the screen or switch to other apps (e.g. start Slacking 🙂)

On Chrome: close event fires
On Firefox: both events fire

I believe Firefox's behaviour is also correct, as a close will always follow an error
https://websockets.spec.whatwg.org/#eventdef-websocket-error

When the WebSocket connection is closed, possibly cleanly, the user agent must queue a task to run the following substeps:

    Change the ready state to CLOSED (3).

    If the user agent was required to fail the WebSocket connection, or if the WebSocket connection was closed after being flagged as full, fire an event named error at the WebSocket object. [WSP]

    Fire an event named close at the WebSocket object, using CloseEvent, with the wasClean attribute initialized to true if the connection closed cleanly and false otherwise, the code attribute initialized to the WebSocket connection close code, and the reason attribute initialized to the result of applying UTF-8 decode without BOM to the WebSocket connection close reason. [WSP]

But I've not worked closely very closely with WebSockets.

That looks like the spec actually requires to fire both. Or someone forget to return on step 2. 🤔

So perhaps one should file an issue on the spec, or add a WPT that checks for getting both events. It's an old spec, so changing it likely will have little impact, unless this behavior is breaking something (which it could be if Chrome only sends one, for example)

Flags: needinfo?(krosylight)
Severity: -- → S4
Priority: -- → P4
Whiteboard: [necko-triaged]

Tested this again, while serving the server in device A and connecting to the server in device B. And disconnect from wifi on device B.

On Android:

  • Chrome fires only close
  • Firefox fires both

On Windows:

  • Chrome fires only close, after a long delay
  • Firefox fires both, also after a long delay

On macOS:

  • Safari immediately fires both events
  • Firefox fires both too after a bit of delay
  • Chrome fires only close event after a long delay

Probably a Chrome bug. I'll file a bug there.

The test script for comment #4:

Deno.serve((req) => {
  if (req.headers.get("upgrade") != "websocket") {
    return new Response(
      `
      <!DOCTYPE html>
      <div id="log"></div>
      <script>
        const onmessage = (message = "") => {
          log.prepend(document.createElement("br"))
          log.prepend(message + " " + Date.now())
        }
        const setup = () => {
          log.prepend(document.createElement("br"))
          log.prepend(Date.now() + " connecting websocket");
          socket = new WebSocket("/");
          socket.onmessage = onmessage;
          socket.onclose = () => onmessage("closed");
          socket.onerror = () => onmessage("error");
        }
        setup();
      </script>
    `,
      {
        headers: {
          "content-type": "text/html",
        },
      },
    );
  }

  const userAgent = req.headers.get("user-agent");

  const { socket, response } = Deno.upgradeWebSocket(req);

  socket.addEventListener("open", () => {
    console.log(`a client connected! ${userAgent}`);
  });

  const handle = setInterval(() => {
    socket.send("ping");
  }, 1000);

  socket.addEventListener("close", () => {
    console.log(`a client disconnected! ${userAgent}`);
    clearTimeout(handle);
  });

  return response;
});
Flags: needinfo?(krosylight)
Status: NEW → RESOLVED
Closed: 2 months ago
Resolution: --- → WONTFIX
You need to log in before you can comment on or make changes to this bug.