This sounds like something that would be easily overlooked and cause disconnections. I feel like the current implementation does the right thing for the vast majority of use cases. I’m not sure protocol-level pings should be expected to have their pongs overridden for the general case. Are there any use cases for a manual pong other than a proxy?
I wasn’t even sure it even made sense for a proxy, but I checked nginx and it did forward my pings to the upstream. However, I don’t know if it does it synchronously.
There are a few possible solutions if you do want this specific behavior. You could define a type inheriting from HTTP::WebSocket and override run with this line removed. If you’re concerned with HTTP::WebSocket changing, you can copy/paste the entire class definition into a type you own the hierarchy for. The entire file is only 213 lines, including whitespace and documentation comments. It’s surprisingly approachable.
If you want to work within the stdlib implementation, though, since pongs are sent after the on_ping block, you can block the pong by waiting for a response from the upstream inside the on_ping block:
require "http"
require "http/web_socket"
require "log"
Log.setup :debug
websocket_upstream = HTTP::WebSocketHandler.new do |ws, context|
log = Log.for("upstream")
# ...
ws.on_ping do |msg|
log.debug &.emit "ping", msg: msg
sleep 100.milliseconds
end
end
upstream = HTTP::Server.new([websocket_upstream])
spawn upstream.listen 1081
websocket_proxy = HTTP::WebSocketHandler.new do |client, context|
log = Log.for("proxy")
upstream_pongs = Channel(String).new(10)
upstream_connection = HTTP::WebSocket.new("ws://localhost:1081")
spawn upstream_connection.run
client.on_ping do |msg|
log.debug &.emit "ping", msg: msg
upstream_connection.ping msg
response = upstream_pongs.receive
log.debug &.emit "pong", msg: response
end
upstream_connection.on_pong do |msg|
upstream_pongs.send msg
end
client.on_close { upstream_connection.close rescue nil }
upstream_connection.on_close { client.close rescue nil }
end
proxy = HTTP::Server.new([websocket_proxy])
spawn proxy.listen 1080
ws = HTTP::WebSocket.new("ws://localhost:1080")
ws.on_pong do |msg|
Log.for("client").debug &.emit "pong", msg: msg
end
spawn ws.run
100.times do |i|
sleep 1.second
ws.ping i.to_s
end
The tradeoff is that this blocks the WebSocket fiber until the pong is received from the upstream, so head-of-line blocking situations could be a consideration. In this example, I added 100ms of latency at the upstream.