Skip to content

WebSocket#stream sending (wrongly) empty frames if io has been flushed #16532

@spuun

Description

@spuun

This example

require "http/web_socket"

ws_handler = HTTP::WebSocketHandler.new do |socket|
  socket.on_message do |message|
    puts "server got: #{message}"
    socket.stream do |io|
      io << message
      io.flush
    end
  end
end

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

spawn do
  address = server.bind_tcp("0.0.0.0", 8080)
  puts "Listening on http://#{address}"
  server.listen
end

Fiber.yield

socket = HTTP::WebSocket.new("localhost", "/", 8080)

received = [] of String

socket.on_binary do |message|
  puts "Received: #{String.new(message)}"
  received << String.new(message)
end
socket.on_close do |code, msg|
  puts "Socket closed: #{code}, #{msg}"
end

spawn do
  socket.run
end

socket.send("Hello")
socket.send("World")

sleep 0.1.seconds

puts "All messages received: #{received}"

will print

All messages received: ["Hello", "", "World", ""]

I get these extra empty messages.

I think it's because of HTTP::WebSocket::Protocol::StreamIO#flush. I'm calling #flush without any argument in the stream block, meaning that final=true. When the block is done #stream will call #flush again but this time with @opcode = Opcode::CONTINUATION (which was set by my flush call) and an empty buffer.

I can fix this by make sure to call flush(false), but that doesn't help I want to use a json builder:

socket.stream do |io|
  JSON.build do |json|
    json.string "hello world"
  end # here io.flush is called
end

One big problem though is that a web browser will close the connection when it receives these empty frames, probably because of the opcode?
Maybe this means that HTTP::WebSocket is too naive and should validate incoming frames better and close the connection too?

I'm running

Crystal 1.18.2 (2025-10-21)

LLVM: 21.1.8
Default target: aarch64-apple-darwin24.6.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:bugA bug in the code. Does not apply to documentation, specs, etc.topic:stdlib:networking

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions