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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions