HTTP::WebSocketHandler Does Not Support/manages WebSocket Sub-protocols

by ADMIN 72 views

Introduction

While attempting to reverse proxy Vite's WebSocket usage (HMR or Hot Module Reload), I encountered an issue where my naĂŻve attempt was getting the JS client (browser) automatically disconnected. After some research, I was able to identify the source of the problem and reduce it to two scripts: a mock server that sends a message once connected, and a TS client that will attempt to connect to it.

The Problem

The issue arises when the client and server do not agree on the sub-protocol to use. In this case, the client is trying to connect with the vite-hmr protocol, but the server is not configured to support it. As a result, the connection is closed immediately.

The Code

To demonstrate this issue, I created a mock server in Crystal that sends a message once connected, and a TS client that attempts to connect to it. The mock server is configured to support the vite-hmr protocol, but the TS client is not configured to use it.

Mock Server (Crystal)

require "http/server"
require "http/web_socket"
require "log"

class LogWSProtocol
  include HTTP::Handler

  def call(context : HTTP::Server::Context)
    Log.debug &.emit(
      "request info",
      sec_websocket_protocol: context.request.headers["sec-websocket-protocol"]?,
    )
    return call_next(context)
  end
end

ws_handler = HTTP::WebSocketHandler.new do |ws, ctx|
  Log.debug { "websocket_handler" }

  ws.on_message do |payload|
    Log.debug &.emit("ws.on_message", payload: payload)
  end

  # fake connected payload
  ws.send(%q({"type":"connected"}))
end

Log.setup(:debug)

server = HTTP::Server.new([
  LogWSProtocol.new,
  ws_handler,
])

Process.on_terminate do
  puts "Shutdown requested."
  server.close
end

ipaddr = server.bind_tcp("0.0.0.0", 5050)
puts "Listening on http://#{ipaddr.address}:#{ipaddr.port}/"
server.listen

TS Client

#!/usr/bin/env bun

const serverUrl = "ws://localhost:5050";

console.log(`Connecting to ${serverUrl}/...`);

// Set up WebSocket connection
const socket = new WebSocket(`${serverUrl}/`, "vite-hmr");

// Handle connection open
socket.addEventListener("open", () => {
  console.log("âś“ Connected to server");
  console.log("Waiting 5 seconds for messages...");

  // Set timeout to disconnect after 5 seconds
  setTimeout(() => {
    console.log("⏱️ 5 seconds elapsed, disconnecting");
    socket.close();
  }, 5000);
});

// Handle receiving messages
socket.addEventListener("message", (event) => {
  console.log(`< Received: ${event.data}`);
});

// Handle connection close
socket.addEventListener("close", () => {
  console.log("âś— Disconnected from server");
  process.exit(0);
});

// Handle errors
socket.addEventListener("error", (error) => {
  console.error("âś— WebSocket Error:", error);
  process.exit(1);
});

The Solution

To address this issue, HTTP::WebSocketHandler needs to be aware of the Sec-WebSocket-Protocol header and respond accordingly. This means changing the initialization signature to include the supported protocols.

Proof of Concept

ws_handler = HTTP::WebSocketHandlerEx.new(
  supported_protocols: ["vite-hmr", "vite-ping"],
) do |ws, ctx|
  # ...
end

Conclusion

The issue arises when the client and server do not agree on the sub-protocol to use. To address this, HTTP::WebSocketHandler needs to be aware of the Sec-WebSocket-Protocol header and respond accordingly. This means changing the initialization signature to include the supported protocols.

Related Issues

There are other issues open like #13239 or #8435 that cover similar issues, but none of them address this specific problem.

Future Work

To make this change more robust, we can add more features to HTTP::WebSocketHandlerEx, such as:

  • Supporting multiple protocols
  • Allowing the user to specify the protocol to use
  • Handling errors and exceptions more robustly

Code

The code for this proof of concept is available in the Crystal repository.

Commit Message

The commit message for this change should be:

Add support for WebSocket sub-protocols to HTTP::WebSocketHandler

API Documentation

The API documentation for HTTP::WebSocketHandlerEx should include the following:

  • A description of the supported_protocols method
  • A description of the initialize method
  • A description of the call method

Testing

To ensure that this change works correctly, we should add tests to cover the following scenarios:

  • Connecting with a supported protocol
  • Connecting with an unsupported protocol
  • Handling errors and exceptions

Conclusion

In conclusion, the issue arises when the client and server do not agree on the sub-protocol to use. To address this, HTTP::WebSocketHandler needs to be aware of the Sec-WebSocket-Protocol header and respond accordingly. This means changing the initialization signature to include the supported protocols.

Q&A

Q: What is the issue with HTTP::WebSocketHandler?

A: The issue with HTTP::WebSocketHandler is that it does not support or manage WebSocket sub-protocols. This means that when a client and server do not agree on the sub-protocol to use, the connection is closed immediately.

Q: What is a WebSocket sub-protocol?

A: A WebSocket sub-protocol is a specific protocol that is used in addition to the standard WebSocket protocol. It is used to negotiate the sub-protocol between the client and server.

Q: Why is this issue important?

A: This issue is important because it can cause problems when trying to establish a WebSocket connection between a client and server. If the client and server do not agree on the sub-protocol to use, the connection will be closed immediately, which can cause errors and exceptions.

Q: How can this issue be fixed?

A: This issue can be fixed by changing the initialization signature of HTTP::WebSocketHandler to include the supported protocols. This will allow the client and server to negotiate the sub-protocol and establish a successful connection.

Q: What is the new API for HTTP::WebSocketHandler?

A: The new API for HTTP::WebSocketHandler includes a supported_protocols method that allows the user to specify the protocols that are supported by the handler. The initialize method has also been updated to include the supported protocols.

Q: How can I use the new API for HTTP::WebSocketHandler?

A: To use the new API for HTTP::WebSocketHandler, you can create a new instance of the handler and specify the supported protocols using the supported_protocols method. For example:

ws_handler = HTTP::WebSocketHandlerEx.new(
  supported_protocols: ["vite-hmr", "vite-ping"],
) do |ws, ctx|
  # ...
end

Q: What are the benefits of using the new API for HTTP::WebSocketHandler?

A: The benefits of using the new API for HTTP::WebSocketHandler include:

  • Improved support for WebSocket sub-protocols
  • Easier negotiation of sub-protocols between client and server
  • Reduced errors and exceptions due to sub-protocol mismatch

Q: Are there any limitations to using the new API for HTTP::WebSocketHandler?

A: Yes, there are some limitations to using the new API for HTTP::WebSocketHandler. These include:

  • The client and server must agree on the sub-protocol to use
  • The sub-protocol must be supported by both the client and server
  • The sub-protocol must be specified in the supported_protocols method

Q: How can I test the new API for HTTP::WebSocketHandler?

A: To test the new API for HTTP::WebSocketHandler, you can create a test client and server that use the new API. You can then test the connection between the client and server using a tool such as curl or a WebSocket client library.

Q: What are the next steps for improving HTTP::WebSocketHandler?

A: The next steps for improving HTTP::WebSocketHandler include:

  • Adding more features to the supported_protocols method
  • Improving error handling and exception handling
  • Adding support for multiple sub-protocols

Conclusion

In conclusion, the issue with HTTP::WebSocketHandler is that it does not support or manage WebSocket sub-protocols. This can cause problems when trying to establish a WebSocket connection between a client and server. The new API for HTTP::WebSocketHandler includes a supported_protocols method that allows the user to specify the protocols that are supported by the handler. This can improve support for WebSocket sub-protocols and reduce errors and exceptions due to sub-protocol mismatch.