Skip to content

ProtocolError raised during close_write when underlying HTTP/2 stream is already closed #23

@davidalejandroaguilar

Description

@davidalejandroaguilar

Problem

Not sure if this is the right repo to raise this error, but I'm getting this error continuously when trying out Falcon with WebSockets over HTTP/2 and just reloading the page:

Protocol::HTTP2::ProtocolError: Cannot send data in state: closed

I first thought this should be raised in async-cable, but I see it already guards for closed connection:

  ensure
      unless @websocket.closed?
          @websocket.close_write(error)
      end
  end

However, @websocket.closed? returns false even when the underlying HTTP/2 stream has been reset by the client.

So I think the flow is:

  1. Client sends HTTP/2 RST_STREAM frame (e.g. page reload)
  2. HTTP/2 layer immediately closes the stream
  3. async-cable cleanup runs and checks @websocket.closed?
  4. Calls @websocket.close_write(error)
  5. close_write calls send_close which tries to write a close frame
  6. Error: Can't send close frame because HTTP/2 stream is already closed

Solution

Maybe close_write should also ignore those errors like close! does?

Though also wondering why @websocket.closed? returns false if Protocol::HTTP2::Stream#close! was already called.

Environment:

  • Ruby: 3.4.7
  • protocol-websocket: 0.20.2
  • protocol-http2: 0.23.0
  • async-http: 0.91.0
  • async-cable: 0.3.0
  • falcon: 0.52.4
  • rails: 8.0.3

Stack trace

{
  "time": "2025-10-18T23:50:49-06:00",
  "severity": "warn",
  "process_id": 95954,
  "fiber_id": 23816,
  "pid": 95954,
  "subject": "Async::Task",
  "object_id": 23824,
  "message": "Task may have ended with unhandled exception.",
  "event": {
    "type": "failure",
    "root": "/Users/david/src/crm",
    "class": "Protocol::HTTP2::ProtocolError",
    "message": "Cannot send data in state: closed",
    "backtrace": [
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http2-0.23.0/lib/protocol/http2/stream.rb:225:in 'Protocol::HTTP2::Stream#send_data'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/output.rb:129:in 'Async::HTTP::Protocol::HTTP2::Output#send_data'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/output.rb:61:in 'Async::HTTP::Protocol::HTTP2::Output#write'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http-0.54.0/lib/protocol/http/body/stream.rb:299:in 'Protocol::HTTP::Body::Stream#write'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/frame.rb:237:in 'Protocol::WebSocket::Frame#write'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/framer.rb:62:in 'Protocol::WebSocket::Framer#write_frame'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/connection.rb:145:in 'Protocol::WebSocket::Connection#write_frame'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/connection.rb:260:in 'Protocol::WebSocket::Connection#send_close'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/connection.rb:105:in 'Protocol::WebSocket::Connection#close_write'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-cable-0.3.0/lib/async/cable/socket.rb:45:in 'block in Async::Cable::Socket#run'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:207:in 'block in Async::Task#run'",
      "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:452:in 'block in Async::Task#schedule'"
    ],
    "cause": {
      "class": "Async::Stop",
      "message": "Task was stopped",
      "backtrace": [
        "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/scheduler.rb:244:in 'IO::Event::Selector::KQueue#transfer'",
        "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/scheduler.rb:244:in 'Async::Scheduler#block'",
        "<internal:thread_sync>:18:in 'Thread::Queue#pop'",
        "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-cable-0.3.0/lib/async/cable/socket.rb:36:in 'block in Async::Cable::Socket#run'",
        "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:207:in 'block in Async::Task#run'",
        "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:452:in 'block in Async::Task#schedule'"
      ],
      "cause": {
        "class": "Async::Stop",
        "message": "Task was stopped",
        "backtrace": [
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/scheduler.rb:244:in 'IO::Event::Selector::KQueue#transfer'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/scheduler.rb:244:in 'Async::Scheduler#block'",
          "<internal:thread_sync>:18:in 'Thread::Queue#pop'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http-0.54.0/lib/protocol/http/body/writable.rb:72:in 'Protocol::HTTP::Body::Writable#read'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/input.rb:22:in 'Async::HTTP::Protocol::HTTP2::Input#read'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http-0.54.0/lib/protocol/http/body/stream.rb:417:in 'Protocol::HTTP::Body::Stream#read_next'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http-0.54.0/lib/protocol/http/body/stream.rb:67:in 'Protocol::HTTP::Body::Stream::Reader#read'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/framer.rb:67:in 'Protocol::WebSocket::Framer#read_header'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/framer.rb:51:in 'Protocol::WebSocket::Framer#read_frame'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/connection.rb:123:in 'Protocol::WebSocket::Connection#read_frame'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-websocket-0.20.2/lib/protocol/websocket/connection.rb:295:in 'Protocol::WebSocket::Connection#read'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-cable-0.3.0/lib/async/cable/middleware.rb:50:in 'Async::Cable::Middleware#handle_incoming_websocket'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-cable-0.3.0/lib/async/cable/middleware.rb:31:in 'block in Async::Cable::Middleware#call'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-websocket-0.30.0/lib/async/websocket/adapters/http.rb:41:in 'block in Async::WebSocket::Adapters::HTTP.open'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/body/hijack.rb:39:in 'Async::HTTP::Body::Hijack#call'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/rack-3.2.3/lib/rack/body_proxy.rb:56:in 'Method#call'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/rack-3.2.3/lib/rack/body_proxy.rb:56:in 'Rack::BodyProxy#method_missing'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/rack-3.2.3/lib/rack/body_proxy.rb:56:in 'Rack::BodyProxy#method_missing'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/rack-3.2.3/lib/rack/body_proxy.rb:56:in 'Rack::BodyProxy#method_missing'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/rack-3.2.3/lib/rack/body_proxy.rb:56:in 'Rack::BodyProxy#method_missing'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http-0.54.0/lib/protocol/http/body/streamable.rb:129:in 'Protocol::HTTP::Body::Streamable::Body#call'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/output.rb:94:in 'Async::HTTP::Protocol::HTTP2::Output#stream'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:207:in 'block in Async::Task#run'",
          "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:452:in 'block in Async::Task#schedule'"
        ],
        "cause": {
          "class": "Async::Stop::Cause",
          "message": "Stopping task!",
          "backtrace": [
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:285:in 'Async::Task#stop'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/output.rb:82:in 'Async::HTTP::Protocol::HTTP2::Output#stop'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/stream.rb:163:in 'Async::HTTP::Protocol::HTTP2::Stream#closed'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/request.rb:85:in 'Async::HTTP::Protocol::HTTP2::Request::Stream#closed'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http2-0.23.0/lib/protocol/http2/stream.rb:256:in 'Protocol::HTTP2::Stream#close!'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http2-0.23.0/lib/protocol/http2/stream.rb:372:in 'Protocol::HTTP2::Stream#receive_reset_stream'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http2-0.23.0/lib/protocol/http2/connection.rb:536:in 'Protocol::HTTP2::Connection#receive_reset_stream'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http2-0.23.0/lib/protocol/http2/reset_stream_frame.rb:51:in 'Protocol::HTTP2::ResetStreamFrame#apply'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/protocol-http2-0.23.0/lib/protocol/http2/connection.rb:181:in 'Protocol::HTTP2::Connection#read_frame'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-http-0.91.0/lib/async/http/protocol/http2/connection.rb:92:in 'block in Async::HTTP::Protocol::HTTP2::Connection#read_in_background'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:207:in 'block in Async::Task#run'",
            "/Users/david/.local/share/mise/installs/ruby/3.4.7/lib/ruby/gems/3.4.0/gems/async-2.34.0/lib/async/task.rb:452:in 'block in Async::Task#schedule'"
          ]
        }
      }
    }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions