How to cleanly "close"/"abort" a websocket connection *before* it connects

234 views Asked by At

I am making a ws connection, but in some cases I need/want to abort the request cleanly, without the console logging an error like this one -

WebSocket connection to 'ws://xyz:3001/? failed: WebSocket is closed before the connection is established.

the code looks like:

const ws = new WebSocket(`${url}`);

setTimeout(() => ws.close(), random); // some other event that occur at any given time "random"

A simple use case is if a connection is initiated when a user opens a view, but then quickly closes the view which could/should abort the connection request.

is there a way to abort the connection request cleanly, so that I don't get too many logs like that in the browser console?

1

There are 1 answers

6
VonC On

You can test and see if the readyState property could help.

You can use it to check the connection's current state before attempting to close it. That would allow you to avoid trying to close the connection if it is not in an open state, thus preventing the error message from appearing in the console.

The WebSocket API provides different states through the readyState attribute:

  • CONNECTING (0): The connection is not yet open.
  • OPEN (1): The connection is open and ready to communicate.
  • CLOSING (2): The connection is in the process of closing.
  • CLOSED (3): The connection is closed or could not be opened.

You can use these states to decide when it is appropriate to close the WebSocket:

const ws = new WebSocket(`${url}`);

setTimeout(() => {
    // Check if the WebSocket's state is CONNECTING or OPEN before attempting to close it
    if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) {
        ws.close();
    }
}, random); // some other event that occur at any given time "random"

That code modification makes sure ws.close() is only called if the WebSocket is either still connecting or already open. If the WebSocket is in the process of closing or is already closed, calling close() again can lead to unwanted errors or logs.

I am 99% sure that calling ws.close() when ws.readyState === WebSocket.CONNECTING, will log an error in the console, I just want to effectively remove the local listener and drop to the void

Given the limitations of the WebSocket API in handling this specific use case directly, you might consider using a flag to prevent actions on early closure.

let proceedWithConnection = true;

const ws = new WebSocket(`${url}`);

// Set up WebSocket event listeners
ws.onopen = () => {
    if (!proceedWithConnection) {
        ws.close(); // Close immediately if we have decided not to proceed
    } else {
        // Proceed with setting up the connection
    }
};

// Some other event that might occur at any given time
setTimeout(() => {
    proceedWithConnection = false;
}, random);

Or you could delay the actual connection attempt until you are more certain you will need it. That could involve setting up a preliminary delay (debouncing the connection attempt, as previously discussed) to make sure the user's action necessitates the WebSocket connection:

let shouldConnect = true;

setTimeout(() => {
    if (shouldConnect) {
        const ws = new WebSocket(`${url}`);
        // Set up your WebSocket connection here
    }
}, delayBeforeConnecting);

// Some event indicates the connection should not proceed
setTimeout(() => {
    shouldConnect = false;
}, random);

That also ties in with the OP's whatwg/websockets issue 57, where Adam Rice (ricea), from the Chromium project, adds:

The WebSocket close() method can already be used to do this. If you don't like Chrome's behaviour of printing a console message when this happens you should file an issue at https://crbug.com/.

The proposed WebSocketStream API uses AbortController for cancelling the handshake already.

We are unlikely to add this to the WebSocket API because it can be easily emulated with a function like

function AbortableWebSocket(url, signal) {
  signal.throwIfAborted();
  const ws = new WebSocket(url);
  const abortHandler = () => ws.close();
  signal.addEventListener('abort', abortHandler);
  ws.addEventListener('open', 
    () => signal.removeEventListener('abort', abortHandler));
  return ws;
}

So the signal.throwIfAborted() call will make sure that, if the operation was already aborted before the WebSocket connection was initiated, it would immediately throw, preventing the connection.
If the abort event is signaled after the connection attempt has started but before it is fully open, it calls ws.close() to terminate the attempt. Once the connection is successfully opened, it removes the abort event listener to clean up resources.

But, as noted by Bergi in the comments, using ws.close() to terminate a WebSocket connection attempt that has not fully opened could still lead to a warning in the developer console.
That is precisely the situation you were aiming to avoid, as the goal is to abort the connection attempt silently, without generating console warnings.

You might consider a slightly different approach, focusing on preventing the connection attempt from proceeding or being initiated based on the abort signal, rather than trying to close an already initiated connection.
However, this approach has limitations because, once a WebSocket connection attempt is made, the API does not provide a built-in, silent abort mechanism.

You can modify Adam Rice's code to integrate the abort logic in a way that minimizes the chances of generating console warnings. You woud introduce a check before initiating the WebSocket connection to make sure the abort signal has not been triggered. And modify the cleanup process to make sure resources are released without necessarily closing the WebSocket connection if it is still in the CONNECTING state.
Instead of closing the WebSocket upon abort, manage the state to prevent further actions on the WebSocket if it is not yet open.

function AbortableWebSocket(url, signal) {
    // Immediately abort if the signal has already been triggered
    if (signal.aborted) {
        console.log("WebSocket connection aborted before initiation.");
        return null; // Or handle this case as appropriate for your application
    }

    const ws = new WebSocket(url);
    let aborted = false; // Track if the connection has been aborted

    const abortHandler = () => {
        console.log("Abort signal received.");
        aborted = true;
        // Instead of calling ws.close(), which can log a warning, manage abort state locally
    };

    signal.addEventListener('abort', abortHandler);

    // Cleanup function to remove all listeners and handle abort state
    const cleanup = () => {
        signal.removeEventListener('abort', abortHandler);
        ws.removeEventListener('open', onOpen);
        ws.removeEventListener('error', onError);
        ws.removeEventListener('close', onClose);

        if (aborted) {
            // Handle any necessary cleanup or state management due to aborting
            console.log("WebSocket connection cleanup after abort.");
        }
    };

    const onOpen = () => {
        if (aborted) {
            // Close the connection only if it is fully open and abort was requested
            ws.close();
        }
        cleanup();
    };

    const onError = () => cleanup();
    const onClose = () => cleanup();

    ws.addEventListener('open', onOpen);
    ws.addEventListener('error', onError);
    ws.addEventListener('close', onClose);

    return ws;
}