HTTP::WebSocketHandler Does Not Support/manages WebSocket Sub-protocols
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.