Hi Tomcat-Team,

since Apple released Safari 15 (both iOS and macOS) I am running into a strange 
issue related to Apache Tomcat, Safari/WebKit and certain sequences of messages 
received via a WebSocket. When the browser receives messages in this order, the 
connection gets closed.

The following sequence triggers the issue:
1. Connect from Safari to a Tomcat WebSocket server and use the 
'permessage-deflate' extension
2. Receive a text message from the WebSocket server
3. Receive a large binary message from the server
4. The WebSocket connection is closed with the close code PROTOCOL_ERROR.

Some further notes/observations:
- I was able to reduce the error to a minimal example (see basic code at the 
end of this mail).
- From my experiments, "large binary message" refers to messages with a byte 
size of at least roughly 8190 bytes.
- If only binary messages (of any size) are received, no issue occurs.
- If only text messages (of any size) are received, no issue occurs.
- If the binary messages are smaller than the given size, no issue occurs.
- Safari since version 15 sends the permessasge-deflate flag and Tomcat accepts 
it. If this flag is not accepted by the server, no issue occurs. However, no 
compression is used in this case (which is unfortunate).
- The issue only seems to affect the combination of Tomcat and Safari. I was 
not able to reproduce the issue with a WebSocket server written in Python or 
with other browsers (Google Chrome/Mozilla Firefox).
- The issue occurred for me with the latest Apache Tomcat versions as of 
2022-03-05: 9.0.62 and 10.0.20. All versions of Safari 15 seem to be affected 
(even the recently released Safari Technology Preview).
- While debugging happened mostly with the server running on Windows, our 
deployed Linux machines are also affected. Safari was running on macOS and on 
an iPad.
- There are/were numerous bugs in Safari 15 related to WebSockets (e.g., 
https://bugs.webkit.org/show_bug.cgi?id=228296). However, the outlined issue 
does not occur if the sequence of messages is served by a WebSocket server 
written in Python.
- The error thrown in Safari: "WebSocket connection to 
'ws://hostname:port/WebSocket/ws' failed: The operation couldn't be completed. 
(kNWErrorDomainPOSIX error 100 - Protocol error)"

Does anyone have an idea or is able to clarify if this is an issue with Tomcat 
or with Safari/WebKit? Or if there is a workaround to this issue?

Best regards,
Florian

Example code Java:

@ServerEndpoint(value = "/ws")
public class WebSocketServer {

        @OnOpen
        public void onOpen(final Session session) throws IOException {
                System.out.println("WebSocket has been opened");
        }

        @OnMessage
        public void onMessage(String message, Session session) {
                System.out.println("Received message: " + message);
                String[] parameters = message.split(",");
                if (parameters.length != 2) {
                        return;
                }

                String type = parameters[0];
                int length = Integer.parseInt(parameters[1]);

                switch (type) {
                        case "text" -> 
session.getAsyncRemote().sendText(getRandomString(length));
                        case "binary" -> {
                                byte[] randomBytes = new byte[length];
                                new Random().nextBytes(randomBytes);
                                
session.getAsyncRemote().sendBinary(ByteBuffer.wrap(randomBytes));
                        }
                        default -> System.out.println("Unknown type " + type);
                }
        }

        private String getRandomString(int length) {
                String AB = 
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
                SecureRandom rnd = new SecureRandom();

                StringBuilder sb = new StringBuilder(length);
                for(int i = 0; i < length; i++)
                        sb.append(AB.charAt(rnd.nextInt(AB.length())));
                return sb.toString();
        }

        @OnClose
        public void onClose(final CloseReason closeReason){
                System.out.println("Session closed; Reason: " + 
closeReason.getCloseCode() + ":" + closeReason.getReasonPhrase());
        }

        @OnError
        public void onError(Session session, Throwable throwable) {
                throwable.printStackTrace();
        }
}

Example Code HTML/Javascript:

<!DOCTYPE html>
<html>
        <button id="connectButton"      type="button" 
onclick="connectToWebSocket()">
                Connect to WebSocket
        </button>

        <br>
        <br>

        <button id="textMessageButton" disabled="true" 
onclick="textMessageTest()">
                Test WebSocket text message
        </button>

        <br>
        <br>

        <button id="binaryMessageButton" disabled="true" 
onclick="binaryMessageTest()">
                Test WebSocket binary message
        </button>

        <script>
                function connectToWebSocket() {
                        const connectButton = 
document.getElementById('connectButton');
                        connectButton.disabled = true;
                        try {
                                this.ws = new 
WebSocket('ws://localhost:8080/webSocket/ws');
                                ws.binaryType = "arraybuffer";
                        } catch (error) {
                                console.warn('Error during WebSocket 
initialization', error);
                        }

                        this.ws.onopen = function() {
                                console.log('Successfully opened the WebSocket 
connection');
                                const textMessageButton = 
document.getElementById('textMessageButton');
                                textMessageButton.disabled = false;

                                const binaryMessageButton = 
document.getElementById('binaryMessageButton');
                                binaryMessageButton.disabled = false;
                        };

                        this.ws.onerror = function(error) {
                                console.log('WebSocket error', error);
                                connectButton.disabled = false;
                        }

                        this.ws.onmessage = function(messageEvent) {
                                console.log('Received message from WebSocket:', 
messageEvent.data);
                        }
                }

                function textMessageTest() {
                        if (!this.ws) {
                                console.warn('No WebSocket available');
                                return;
                        }

                        this.ws.send('text,1024');
                }

                function binaryMessageTest() {
                        if (!this.ws) {
                                console.warn('No WebSocket available');
                                return;
                        }

                        this.ws.send('binary,16384');
                }
        </script>
</html>

---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscr...@tomcat.apache.org
For additional commands, e-mail: users-h...@tomcat.apache.org

Reply via email to