<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roshan Melvin</title>
    <description>The latest articles on DEV Community by Roshan Melvin (@roshan_melvin).</description>
    <link>https://dev.to/roshan_melvin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3853185%2F49bbc22f-7114-46f1-9d24-0d3cb255d712.png</url>
      <title>DEV Community: Roshan Melvin</title>
      <link>https://dev.to/roshan_melvin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roshan_melvin"/>
    <language>en</language>
    <item>
      <title>Why your WebSocket messages silently vanish - the channel.ready trap in Dart</title>
      <dc:creator>Roshan Melvin</dc:creator>
      <pubDate>Tue, 31 Mar 2026 09:56:36 +0000</pubDate>
      <link>https://dev.to/roshan_melvin/why-your-websocket-messages-silently-vanish-the-channelready-trap-in-dart-3mi5</link>
      <guid>https://dev.to/roshan_melvin/why-your-websocket-messages-silently-vanish-the-channelready-trap-in-dart-3mi5</guid>
      <description>&lt;h1&gt;
  
  
  Why Your WebSocket Messages Silently Vanish - The &lt;code&gt;channel.ready&lt;/code&gt; Trap in Dart
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; dart, flutter, websocket, networking&lt;/p&gt;




&lt;p&gt;I was building native WebSocket support for &lt;a href="https://github.com/foss42/apidash" rel="noopener noreferrer"&gt;API Dash&lt;/a&gt; - an open-source Flutter API client - as part of my GSoC 2026 proposal. The implementation looked clean. Connect, send, receive. I tested it against &lt;code&gt;wss://echo.websocket.events&lt;/code&gt;, typed a message immediately after connecting, and watched the response feed. Nothing came back. No error. No exception. The send call returned successfully. The server just never saw my message.&lt;/p&gt;

&lt;p&gt;This took me longer to debug than I'd like to admit. Here's exactly what's happening and how to fix it in one line.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three-Phase Handshake You're Skipping
&lt;/h2&gt;

&lt;p&gt;Most developers think of a WebSocket connection as a single step. It isn't. Under the hood, establishing a usable WebSocket channel requires three sequential phases to complete:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 - TCP Handshake&lt;/strong&gt;&lt;br&gt;
The standard three-way TCP handshake: SYN → SYN-ACK → ACK. Your OS handles this. Takes milliseconds on a local network, longer on mobile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 - TLS Handshake (if using wss://)&lt;/strong&gt;&lt;br&gt;
ClientHello → ServerHello → Certificate exchange → Finished. This alone can take 100–300ms on a cold connection. If you are connecting to any &lt;code&gt;wss://&lt;/code&gt; endpoint, this phase is mandatory before a single application byte can flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 - HTTP Upgrade (RFC 6455 §4)&lt;/strong&gt;&lt;br&gt;
The WebSocket protocol itself begins as an HTTP/1.1 request. The client sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/chat&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Upgrade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;websocket&lt;/span&gt;
&lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt;
&lt;span class="na"&gt;Sec-WebSocket-Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dGhlIHNhbXBsZSBub25jZQ==&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server must respond with &lt;code&gt;101 Switching Protocols&lt;/code&gt; before the connection becomes a true WebSocket channel. This is the handshake defined in &lt;a href="https://datatracker.ietf.org/doc/html/rfc6455#section-4" rel="noopener noreferrer"&gt;RFC 6455 §4&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Only after all three phases complete is the channel in a readable and writable state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Dart's &lt;code&gt;WebSocketChannel.connect()&lt;/code&gt; Leaves You
&lt;/h2&gt;

&lt;p&gt;Here is the trap. In Dart's &lt;code&gt;web_socket_channel&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebSocketChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// channel object exists HERE&lt;/span&gt;
&lt;span class="c1"&gt;// but the socket is NOT usable yet&lt;/span&gt;
&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'hello'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// silently dropped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connect()&lt;/code&gt; returns &lt;strong&gt;immediately after Phase 1&lt;/strong&gt; - after the TCP handshake. The &lt;code&gt;channel&lt;/code&gt; object is constructed and handed to you. But Phase 2 (TLS) and Phase 3 (HTTP Upgrade) are still in progress asynchronously. When you write to &lt;code&gt;channel.sink&lt;/code&gt; before the upgrade completes, the bytes enter the send buffer. The buffer does not throw. The buffer does not warn you. The server discards them because it is still processing the upgrade handshake and is not yet in WebSocket frame mode.&lt;/p&gt;

&lt;p&gt;This is the worst kind of bug - it fails silently, returns no error, and only manifests under timing conditions that vary by network latency and TLS session reuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix - One Line
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;web_socket_channel&lt;/code&gt; package exposes a &lt;code&gt;Future&lt;/code&gt; called &lt;code&gt;channel.ready&lt;/code&gt; that resolves only after all three phases complete - TCP, TLS, and HTTP Upgrade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebSocketChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// waits for full RFC 6455 §4 handshake&lt;/span&gt;
&lt;span class="c1"&gt;// NOW safe to write&lt;/span&gt;
&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'hello'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;await&lt;/code&gt; is the entire fix. In my implementation for API Dash, I gate the UI send button behind this future and attach the incoming stream listener only after it resolves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Uri&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebSocketChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_channels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_emitStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_emitMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;incoming:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;onError:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_emitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;onDone:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_emitStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disconnected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_emitError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees zero dropped frames during TLS negotiation, and maps HTTP upgrade failures to readable error events instead of silent drops.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Killed Previous API Dash WebSocket PRs
&lt;/h2&gt;

&lt;p&gt;After tracing this bug I went back and read every closed WebSocket PR in the API Dash repository - #210, #215, #555. None of them awaited &lt;code&gt;channel.ready&lt;/code&gt;. All of them reported the same symptom during review: messages sent immediately after connecting were unreliable. The reviewers attributed it to state management issues and closed the PRs. The actual root cause was this timing gap - a spec-level detail invisible unless you read RFC 6455 §4 carefully.&lt;/p&gt;

&lt;p&gt;My implementation in &lt;a href="https://github.com/foss42/apidash/pull/1529" rel="noopener noreferrer"&gt;PR #1529&lt;/a&gt; fixes this at the connection manager level so the UI layer never has to think about handshake timing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;If you are using &lt;code&gt;web_socket_channel&lt;/code&gt; in Dart or Flutter and your early messages are disappearing - add &lt;code&gt;await channel.ready&lt;/code&gt; before your first write. One line. RFC 6455 §4. Done.&lt;/p&gt;

</description>
      <category>dart</category>
      <category>flutter</category>
      <category>websocket</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
