]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
websocket: handling of PONG frames
authorStefan Eissing <stefan@eissing.org>
Thu, 4 Sep 2025 14:09:05 +0000 (16:09 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 5 Sep 2025 11:17:39 +0000 (13:17 +0200)
The auto PONG frames were inserted into the connection at the time
a PING had been decoded, irregardless if an upstream frame was just
in the middle of being assembled.

Add PONG frames only to the buffer if there is no frame currently
assemebled and, if it is, set the control frame aside. This control
frame is then added on the first opportunity of a "clean" send buffer.

There is only a single control frame set aside at a time. This means
a double PING will, when the PONG cannot be sent right away, only
send the last PONG.

I imagine this is fine. We want to prevent the endless buffering of
PONG frames on a connection where the server sends but does no receives.

Reported-by: Calvin Ruocco
Fixes #16706
Closes #18479

lib/ws.c

index 229539c3f3b257fc76f0d3b777d69ecac53877b4..e973409b6bf644bb40ebcc3dcc1fa73c02bba97c 100644 (file)
--- a/lib/ws.c
+++ b/lib/ws.c
@@ -108,6 +108,15 @@ struct ws_encoder {
   BIT(contfragment); /* set TRUE if the previous fragment sent was not final */
 };
 
+/* Control frames are allowed up to 125 characters, rfc6455, ch. 5.5 */
+#define WS_MAX_CNTRL_LEN    125
+
+struct ws_cntrl_frame {
+  unsigned int type;
+  size_t payload_len;
+  unsigned char payload[WS_MAX_CNTRL_LEN];
+};
+
 /* A websocket connection with en- and decoder that treat frames
  * and keep track of boundaries. */
 struct websocket {
@@ -117,6 +126,7 @@ struct websocket {
   struct bufq recvbuf;    /* raw data from the server */
   struct bufq sendbuf;    /* raw data to be sent to the server */
   struct curl_ws_frame recvframe;  /* the current WS FRAME received */
+  struct ws_cntrl_frame pending; /* a control frame pending to be sent */
   size_t sendbuf_payload; /* number of payload bytes in sendbuf */
 };
 
@@ -229,7 +239,7 @@ static CURLcode ws_frame_flags2firstbyte(struct Curl_easy *data,
   switch(flags & ~CURLWS_OFFSET) {
     case 0:
       if(contfragment) {
-        infof(data, "[WS] no flags given; interpreting as continuation "
+        CURL_TRC_WS(data, "no flags given; interpreting as continuation "
                     "fragment for compatibility");
         *pfirstbyte = (WSBIT_OPCODE_CONT | WSBIT_FIN);
         return CURLE_OK;
@@ -316,12 +326,12 @@ static CURLcode ws_send_raw_blocking(struct Curl_easy *data,
                                      struct websocket *ws,
                                      const char *buffer, size_t buflen);
 
-typedef ssize_t ws_write_payload(const unsigned char *buf, size_t buflen,
-                                 int frame_age, int frame_flags,
-                                 curl_off_t payload_offset,
-                                 curl_off_t payload_len,
-                                 void *userp,
-                                 CURLcode *err);
+typedef CURLcode ws_write_payload(const unsigned char *buf, size_t buflen,
+                                  int frame_age, int frame_flags,
+                                  curl_off_t payload_offset,
+                                  curl_off_t payload_len,
+                                  void *userp,
+                                  size_t *pnwritten);
 
 static void ws_dec_next_frame(struct ws_decoder *dec)
 {
@@ -390,21 +400,20 @@ static CURLcode ws_dec_read_head(struct ws_decoder *dec,
         ws_dec_reset(dec);
         return CURLE_RECV_ERROR;
       }
-      if(dec->frame_flags & CURLWS_PING && dec->head[1] > 125) {
+      if(dec->frame_flags & CURLWS_PING && dec->head[1] > WS_MAX_CNTRL_LEN) {
         /* The maximum valid size of PING frames is 125 bytes.
            Accepting overlong pings would mean sending equivalent pongs! */
         failf(data, "[WS] received PING frame is too big");
         ws_dec_reset(dec);
         return CURLE_RECV_ERROR;
       }
-      if(dec->frame_flags & CURLWS_PONG && dec->head[1] > 125) {
+      if(dec->frame_flags & CURLWS_PONG && dec->head[1] > WS_MAX_CNTRL_LEN) {
         /* The maximum valid size of PONG frames is 125 bytes. */
         failf(data, "[WS] received PONG frame is too big");
         ws_dec_reset(dec);
         return CURLE_RECV_ERROR;
       }
-      if(dec->frame_flags & CURLWS_CLOSE && dec->head[1] > 125) {
-        /* The maximum valid size of CLOSE frames is 125 bytes. */
+      if(dec->frame_flags & CURLWS_CLOSE && dec->head[1] > WS_MAX_CNTRL_LEN) {
         failf(data, "[WS] received CLOSE frame is too big");
         ws_dec_reset(dec);
         return CURLE_RECV_ERROR;
@@ -479,7 +488,7 @@ static CURLcode ws_dec_pass_payload(struct ws_decoder *dec,
 {
   const unsigned char *inbuf;
   size_t inlen;
-  ssize_t nwritten;
+  size_t nwritten;
   CURLcode result;
   curl_off_t remain = dec->payload_len - dec->payload_offset;
 
@@ -487,15 +496,15 @@ static CURLcode ws_dec_pass_payload(struct ws_decoder *dec,
   while(remain && Curl_bufq_peek(inraw, &inbuf, &inlen)) {
     if((curl_off_t)inlen > remain)
       inlen = (size_t)remain;
-    nwritten = write_cb(inbuf, inlen, dec->frame_age, dec->frame_flags,
-                        dec->payload_offset, dec->payload_len,
-                        write_ctx, &result);
-    if(nwritten < 0)
+    result = write_cb(inbuf, inlen, dec->frame_age, dec->frame_flags,
+                      dec->payload_offset, dec->payload_len,
+                      write_ctx, &nwritten);
+    if(result)
       return result;
-    Curl_bufq_skip(inraw, (size_t)nwritten);
-    dec->payload_offset += (curl_off_t)nwritten;
+    Curl_bufq_skip(inraw, nwritten);
+    dec->payload_offset += nwritten;
     remain = dec->payload_len - dec->payload_offset;
-    CURL_TRC_WS(data, "passed %zd bytes payload, %"
+    CURL_TRC_WS(data, "passed %zu bytes payload, %"
                 FMT_OFF_T " remain", nwritten, remain);
   }
 
@@ -532,12 +541,12 @@ static CURLcode ws_dec_pass(struct ws_decoder *dec,
     /* head parsing done */
     dec->state = WS_DEC_PAYLOAD;
     if(dec->payload_len == 0) {
-      ssize_t nwritten;
+      size_t nwritten;
       const unsigned char tmp = '\0';
       /* special case of a 0 length frame, need to write once */
-      nwritten = write_cb(&tmp, 0, dec->frame_age, dec->frame_flags,
-                          0, 0, write_ctx, &result);
-      if(nwritten < 0)
+      result = write_cb(&tmp, 0, dec->frame_age, dec->frame_flags,
+                          0, 0, write_ctx, &nwritten);
+      if(result)
         return result;
       dec->state = WS_DEC_INIT;
       break;
@@ -602,43 +611,82 @@ struct ws_cw_dec_ctx {
   int cw_type;
 };
 
-static ssize_t ws_cw_dec_next(const unsigned char *buf, size_t buflen,
-                              int frame_age, int frame_flags,
-                              curl_off_t payload_offset,
-                              curl_off_t payload_len,
-                              void *user_data,
-                              CURLcode *err)
+static CURLcode ws_flush(struct Curl_easy *data, struct websocket *ws,
+                         bool blocking);
+static CURLcode ws_enc_send(struct Curl_easy *data,
+                            struct websocket *ws,
+                            const unsigned char *buffer,
+                            size_t buflen,
+                            curl_off_t fragsize,
+                            unsigned int flags,
+                            size_t *sent);
+static CURLcode ws_enc_add_pending(struct Curl_easy *data,
+                                   struct websocket *ws);
+
+static CURLcode ws_enc_add_cntrl(struct Curl_easy *data,
+                                 struct websocket *ws,
+                                 const unsigned char *payload,
+                                 size_t plen,
+                                 unsigned int frame_type)
+{
+  DEBUGASSERT(plen <= WS_MAX_CNTRL_LEN);
+  if(plen > WS_MAX_CNTRL_LEN)
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+
+  /* Overwrite any pending frame with the new one, we keep
+   * only one. */
+  ws->pending.type = frame_type;
+  ws->pending.payload_len = plen;
+  memcpy(ws->pending.payload, payload, plen);
+
+  if(!ws->enc.payload_remain) { /* not in the middle of another frame */
+    CURLcode result = ws_enc_add_pending(data, ws);
+    if(!result)
+      (void)ws_flush(data, ws, Curl_is_in_callback(data));
+    return result;
+  }
+  return CURLE_OK;
+}
+
+static CURLcode ws_cw_dec_next(const unsigned char *buf, size_t buflen,
+                               int frame_age, int frame_flags,
+                               curl_off_t payload_offset,
+                               curl_off_t payload_len,
+                               void *user_data,
+                               size_t *pnwritten)
 {
   struct ws_cw_dec_ctx *ctx = user_data;
   struct Curl_easy *data = ctx->data;
   struct websocket *ws = ctx->ws;
   bool auto_pong = !data->set.ws_no_auto_pong;
   curl_off_t remain = (payload_len - (payload_offset + buflen));
+  CURLcode result;
 
   (void)frame_age;
+  *pnwritten = 0;
 
   if(auto_pong && (frame_flags & CURLWS_PING) && !remain) {
     /* auto-respond to PINGs, only works for single-frame payloads atm */
-    size_t bytes;
-    infof(data, "[WS] auto-respond to PING with a PONG");
+    CURL_TRC_WS(data, "auto PONG to [PING payload=%" FMT_OFF_T
+                "/%" FMT_OFF_T "]", payload_offset, payload_len);
     /* send back the exact same content as a PONG */
-    *err = curl_ws_send(data, buf, buflen, &bytes, 0, CURLWS_PONG);
-    if(*err)
-      return -1;
+    result = ws_enc_add_cntrl(data, ws, buf, buflen, CURLWS_PONG);
+    if(result)
+      return result;
   }
   else if(buflen || !remain) {
     /* forward the decoded frame to the next client writer. */
     update_meta(ws, frame_age, frame_flags, payload_offset,
                 payload_len, buflen);
 
-    *err = Curl_cwriter_write(data, ctx->next_writer,
+    result = Curl_cwriter_write(data, ctx->next_writer,
                               (ctx->cw_type | CLIENTWRITE_0LEN),
                               (const char *)buf, buflen);
-    if(*err)
-      return -1;
+    if(result)
+      return result;
   }
-  *err = CURLE_OK;
-  return (ssize_t)buflen;
+  *pnwritten = buflen;
+  return CURLE_OK;
 }
 
 static CURLcode ws_cw_write(struct Curl_easy *data,
@@ -664,7 +712,7 @@ static CURLcode ws_cw_write(struct Curl_easy *data,
     result = Curl_bufq_write(&ctx->buf, (const unsigned char *)buf,
                              nbytes, &nwritten);
     if(result) {
-      infof(data, "WS: error adding data to buffer %d", result);
+      infof(data, "[WS] error adding data to buffer %d", result);
       return result;
     }
   }
@@ -753,7 +801,7 @@ static void ws_enc_init(struct ws_encoder *enc)
      +---------------------------------------------------------------+
 */
 
-static CURLcode ws_enc_write_head(struct Curl_easy *data,
+static CURLcode ws_enc_add_frame(struct Curl_easy *data,
                                  struct ws_encoder *enc,
                                  unsigned int flags,
                                  curl_off_t payload_len,
@@ -787,18 +835,15 @@ static CURLcode ws_enc_write_head(struct Curl_easy *data,
     enc->contfragment = (flags & CURLWS_CONT) ? (bit)TRUE : (bit)FALSE;
   }
 
-  if(flags & CURLWS_PING && payload_len > 125) {
-    /* The maximum valid size of PING frames is 125 bytes. */
+  if(flags & CURLWS_PING && payload_len > WS_MAX_CNTRL_LEN) {
     failf(data, "[WS] given PING frame is too big");
     return CURLE_TOO_LARGE;
   }
-  if(flags & CURLWS_PONG && payload_len > 125) {
-    /* The maximum valid size of PONG frames is 125 bytes. */
+  if(flags & CURLWS_PONG && payload_len > WS_MAX_CNTRL_LEN) {
     failf(data, "[WS] given PONG frame is too big");
     return CURLE_TOO_LARGE;
   }
-  if(flags & CURLWS_CLOSE && payload_len > 125) {
-    /* The maximum valid size of CLOSE frames is 125 bytes. */
+  if(flags & CURLWS_CLOSE && payload_len > WS_MAX_CNTRL_LEN) {
     failf(data, "[WS] given CLOSE frame is too big");
     return CURLE_TOO_LARGE;
   }
@@ -847,6 +892,23 @@ static CURLcode ws_enc_write_head(struct Curl_easy *data,
   return CURLE_OK;
 }
 
+static CURLcode ws_enc_write_head(struct Curl_easy *data,
+                                  struct websocket *ws,
+                                  struct ws_encoder *enc,
+                                  unsigned int flags,
+                                  curl_off_t payload_len,
+                                  struct bufq *out)
+{
+  /* starting a new frame, we want a clean sendbuf.
+   * Any pending control frame we can add now as part of the flush. */
+  if(ws->pending.type) {
+    CURLcode result = ws_enc_add_pending(data, ws);
+    if(result)
+      return result;
+  }
+  return ws_enc_add_frame(data, enc, flags, payload_len, out);
+}
+
 static CURLcode ws_enc_write_payload(struct ws_encoder *enc,
                                      struct Curl_easy *data,
                                      const unsigned char *buf, size_t buflen,
@@ -881,6 +943,139 @@ static CURLcode ws_enc_write_payload(struct ws_encoder *enc,
   return CURLE_OK;
 }
 
+static CURLcode ws_enc_add_pending(struct Curl_easy *data,
+                                   struct websocket *ws)
+{
+  CURLcode result;
+  size_t n;
+
+  if(!ws->pending.type) /* no pending frame here */
+    return CURLE_OK;
+  if(ws->enc.payload_remain) /* in the middle of another frame */
+    return CURLE_AGAIN;
+
+  result = ws_enc_add_frame(data, &ws->enc, ws->pending.type,
+                            (curl_off_t)ws->pending.payload_len,
+                            &ws->sendbuf);
+  if(result) {
+    CURL_TRC_WS(data, "ws_enc_cntrl(), error addiong head: %d",
+                result);
+    goto out;
+  }
+  result = ws_enc_write_payload(&ws->enc, data, ws->pending.payload,
+                                ws->pending.payload_len,
+                                &ws->sendbuf, &n);
+  if(result) {
+    CURL_TRC_WS(data, "ws_enc_cntrl(), error adding payload: %d",
+                result);
+    goto out;
+  }
+  /* our buffer should always be able to take in a control frame */
+  DEBUGASSERT(n == ws->pending.payload_len);
+  DEBUGASSERT(!ws->enc.payload_remain);
+
+out:
+  memset(&ws->pending, 0, sizeof(ws->pending));
+  return result;
+}
+
+static CURLcode ws_enc_send(struct Curl_easy *data,
+                            struct websocket *ws,
+                            const unsigned char *buffer,
+                            size_t buflen,
+                            curl_off_t fragsize,
+                            unsigned int flags,
+                            size_t *pnsent)
+{
+  size_t n;
+  CURLcode result = CURLE_OK;
+
+  DEBUGASSERT(!data->set.ws_raw_mode);
+  *pnsent = 0;
+
+  if(ws->enc.payload_remain || !Curl_bufq_is_empty(&ws->sendbuf)) {
+    /* a frame is ongoing with payload buffered or more payload
+     * that needs to be encoded into the buffer */
+    if(buflen < ws->sendbuf_payload) {
+      /* We have been called with LESS buffer data than before. This
+       * is not how it's supposed too work. */
+      failf(data, "[WS] curl_ws_send() called with smaller 'buflen' than "
+            "bytes already buffered in previous call, %zu vs %zu",
+            buflen, ws->sendbuf_payload);
+      return CURLE_BAD_FUNCTION_ARGUMENT;
+    }
+    if((curl_off_t)buflen >
+       (ws->enc.payload_remain + (curl_off_t)ws->sendbuf_payload)) {
+      /* too large buflen beyond payload length of frame */
+      failf(data, "[WS] unaligned frame size (sending %zu instead of %"
+                  FMT_OFF_T ")",
+            buflen, ws->enc.payload_remain + ws->sendbuf_payload);
+      return CURLE_BAD_FUNCTION_ARGUMENT;
+    }
+  }
+  else {
+    result = ws_flush(data, ws, Curl_is_in_callback(data));
+    if(result)
+      return result;
+
+    result = ws_enc_write_head(data, ws, &ws->enc, flags,
+                               (flags & CURLWS_OFFSET) ?
+                               fragsize : (curl_off_t)buflen,
+                               &ws->sendbuf);
+    if(result) {
+      CURL_TRC_WS(data, "curl_ws_send(), error writing frame head %d", result);
+      return result;
+    }
+  }
+
+  /* While there is either sendbuf to flush OR more payload to encode... */
+  while(!Curl_bufq_is_empty(&ws->sendbuf) || (buflen > ws->sendbuf_payload)) {
+    /* Try to add more payload to sendbuf */
+    if(buflen > ws->sendbuf_payload) {
+      size_t prev_len = Curl_bufq_len(&ws->sendbuf);
+      result = ws_enc_write_payload(&ws->enc, data,
+                                    buffer + ws->sendbuf_payload,
+                                    buflen - ws->sendbuf_payload,
+                                    &ws->sendbuf, &n);
+      if(result && (result != CURLE_AGAIN))
+        return result;
+      ws->sendbuf_payload += Curl_bufq_len(&ws->sendbuf) - prev_len;
+      if(!ws->sendbuf_payload) {
+        return CURLE_AGAIN;
+      }
+    }
+
+    /* flush, blocking when in callback */
+    result = ws_flush(data, ws, Curl_is_in_callback(data));
+    if(!result && ws->sendbuf_payload > 0) {
+      *pnsent += ws->sendbuf_payload;
+      buffer += ws->sendbuf_payload;
+      buflen -= ws->sendbuf_payload;
+      ws->sendbuf_payload = 0;
+    }
+    else if(result == CURLE_AGAIN) {
+      if(ws->sendbuf_payload > Curl_bufq_len(&ws->sendbuf)) {
+        /* blocked, part of payload bytes remain, report length
+         * that we managed to send. */
+        size_t flushed = (ws->sendbuf_payload - Curl_bufq_len(&ws->sendbuf));
+        *pnsent += flushed;
+        ws->sendbuf_payload -= flushed;
+        return CURLE_OK;
+      }
+      else {
+        /* blocked before sending headers or 1st payload byte. We cannot report
+         * OK on 0-length send (caller counts only payload) and EAGAIN */
+        CURL_TRC_WS(data, "EAGAIN flushing sendbuf, payload_encoded: %zu/%zu",
+                    ws->sendbuf_payload, buflen);
+        DEBUGASSERT(*pnsent == 0);
+        return CURLE_AGAIN;
+      }
+    }
+    else
+      return result;  /* real error sending the data */
+  }
+  return CURLE_OK;
+}
 
 struct cr_ws_ctx {
   struct Curl_creader super;
@@ -959,7 +1154,7 @@ static CURLcode cr_ws_read(struct Curl_easy *data,
 
     if(!ws->enc.payload_remain && Curl_bufq_is_empty(&ws->sendbuf)) {
       /* encode the data as a new BINARY frame */
-      result = ws_enc_write_head(data, &ws->enc, CURLWS_BINARY, nread,
+      result = ws_enc_write_head(data, ws, &ws->enc, CURLWS_BINARY, nread,
                                  &ws->sendbuf);
       if(result)
         goto out;
@@ -1229,6 +1424,7 @@ out:
 
 struct ws_collect {
   struct Curl_easy *data;
+  struct websocket *ws;
   unsigned char *buffer;
   size_t buflen;
   size_t bufidx;
@@ -1239,19 +1435,20 @@ struct ws_collect {
   bool written;
 };
 
-static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen,
-                                 int frame_age, int frame_flags,
-                                 curl_off_t payload_offset,
-                                 curl_off_t payload_len,
-                                 void *userp,
-                                 CURLcode *err)
+static CURLcode ws_client_collect(const unsigned char *buf, size_t buflen,
+                                  int frame_age, int frame_flags,
+                                  curl_off_t payload_offset,
+                                  curl_off_t payload_len,
+                                  void *userp,
+                                  size_t *pnwritten)
 {
   struct ws_collect *ctx = userp;
   struct Curl_easy *data = ctx->data;
   bool auto_pong = !data->set.ws_no_auto_pong;
-  size_t nwritten;
   curl_off_t remain = (payload_len - (payload_offset + buflen));
+  CURLcode result = CURLE_OK;
 
+  *pnwritten = 0;
   if(!ctx->bufidx) {
     /* first write */
     ctx->frame_age = frame_age;
@@ -1262,31 +1459,30 @@ static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen,
 
   if(auto_pong && (frame_flags & CURLWS_PING) && !remain) {
     /* auto-respond to PINGs, only works for single-frame payloads atm */
-    size_t bytes;
-    infof(ctx->data, "[WS] auto-respond to PING with a PONG");
+    CURL_TRC_WS(data, "auto PONG to [PING payload=%" FMT_OFF_T
+                "/%" FMT_OFF_T "]", payload_offset, payload_len);
     /* send back the exact same content as a PONG */
-    *err = curl_ws_send(ctx->data, buf, buflen, &bytes, 0, CURLWS_PONG);
-    if(*err)
-      return -1;
-    nwritten = bytes;
+    result = ws_enc_add_cntrl(ctx->data, ctx->ws, buf, buflen, CURLWS_PONG);
+    if(result)
+      return result;
+    *pnwritten = buflen;
   }
   else {
+    size_t write_len;
+
     ctx->written = TRUE;
     DEBUGASSERT(ctx->buflen >= ctx->bufidx);
-    nwritten = CURLMIN(buflen, ctx->buflen - ctx->bufidx);
-    if(!nwritten) {
-      if(!buflen) {  /* 0 length write, we accept that */
-        *err = CURLE_OK;
-        return 0;
-      }
-      *err = CURLE_AGAIN;  /* no more space */
-      return -1;
+    write_len = CURLMIN(buflen, ctx->buflen - ctx->bufidx);
+    if(!write_len) {
+      if(!buflen)  /* 0 length write, we accept that */
+        return CURLE_OK;
+      return CURLE_AGAIN;  /* no more space */
     }
-    *err = CURLE_OK;
-    memcpy(ctx->buffer + ctx->bufidx, buf, nwritten);
-    ctx->bufidx += nwritten;
+    memcpy(ctx->buffer + ctx->bufidx, buf, write_len);
+    ctx->bufidx += write_len;
+    *pnwritten = write_len;
   }
-  return nwritten;
+  return result;
 }
 
 static CURLcode nw_in_recv(void *reader_ctx,
@@ -1334,6 +1530,7 @@ CURLcode curl_ws_recv(CURL *d, void *buffer,
 
   memset(&ctx, 0, sizeof(ctx));
   ctx.data = data;
+  ctx.ws = ws;
   ctx.buffer = buffer;
   ctx.buflen = buflen;
 
@@ -1384,6 +1581,13 @@ CURLcode curl_ws_recv(CURL *d, void *buffer,
                FMT_OFF_T ", %" FMT_OFF_T " left)",
                buflen, *nread, ws->recvframe.offset,
                ws->recvframe.bytesleft);
+  /* all's well, try to send any pending control. we do not know
+   * when the application will call `curl_ws_send()` again. */
+  if(!data->set.ws_raw_mode && ws->pending.type) {
+    CURLcode r2 = ws_enc_add_pending(data, ws);
+    if(!r2)
+      (void)ws_flush(data, ws, Curl_is_in_callback(data));
+  }
   return CURLE_OK;
 }
 
@@ -1531,9 +1735,10 @@ CURLcode curl_ws_send(CURL *d, const void *buffer_arg,
 {
   struct websocket *ws;
   const unsigned char *buffer = buffer_arg;
-  size_t n;
   CURLcode result = CURLE_OK;
   struct Curl_easy *data = d;
+  size_t ndummy;
+  size_t *pnsent = sent ? sent : &ndummy;
 
   if(!GOOD_EASY_HANDLE(data))
     return CURLE_BAD_FUNCTION_ARGUMENT;
@@ -1541,8 +1746,7 @@ CURLcode curl_ws_send(CURL *d, const void *buffer_arg,
               ", flags=%x), raw=%d",
               buflen, fragsize, flags, data->set.ws_raw_mode);
 
-  if(sent)
-    *sent = 0;
+  *pnsent = 0;
 
   if(!buffer && buflen) {
     failf(data, "[WS] buffer is NULL when buflen is not");
@@ -1586,107 +1790,18 @@ CURLcode curl_ws_send(CURL *d, const void *buffer_arg,
       failf(data, "[WS] fragsize and flags must be zero in raw mode");
       return CURLE_BAD_FUNCTION_ARGUMENT;
     }
-    result = ws_send_raw(data, buffer, buflen, sent);
+    result = ws_send_raw(data, buffer, buflen, pnsent);
     goto out;
   }
 
-  /* Not RAW mode, buf we do the frame encoding */
-
-  if(ws->enc.payload_remain || !Curl_bufq_is_empty(&ws->sendbuf)) {
-    /* a frame is ongoing with payload buffered or more payload
-     * that needs to be encoded into the buffer */
-    if(buflen < ws->sendbuf_payload) {
-      /* We have been called with LESS buffer data than before. This
-       * is not how it's supposed too work. */
-      failf(data, "[WS] curl_ws_send() called with smaller 'buflen' than "
-            "bytes already buffered in previous call, %zu vs %zu",
-            buflen, ws->sendbuf_payload);
-      result = CURLE_BAD_FUNCTION_ARGUMENT;
-      goto out;
-    }
-    if((curl_off_t)buflen >
-       (ws->enc.payload_remain + (curl_off_t)ws->sendbuf_payload)) {
-      /* too large buflen beyond payload length of frame */
-      failf(data, "[WS] unaligned frame size (sending %zu instead of %"
-                  FMT_OFF_T ")",
-            buflen, ws->enc.payload_remain + ws->sendbuf_payload);
-      result = CURLE_BAD_FUNCTION_ARGUMENT;
-      goto out;
-    }
-  }
-  else {
-    /* starting a new frame, we want a clean sendbuf */
-    result = ws_flush(data, ws, Curl_is_in_callback(data));
-    if(result)
-      goto out;
-
-    result = ws_enc_write_head(data, &ws->enc, flags,
-                               (flags & CURLWS_OFFSET) ?
-                               fragsize : (curl_off_t)buflen,
-                               &ws->sendbuf);
-    if(result) {
-      CURL_TRC_WS(data, "curl_ws_send(), error writing frame head %d", result);
-      goto out;
-    }
-  }
-
-  /* While there is either sendbuf to flush OR more payload to encode... */
-  while(!Curl_bufq_is_empty(&ws->sendbuf) || (buflen > ws->sendbuf_payload)) {
-    /* Try to add more payload to sendbuf */
-    if(buflen > ws->sendbuf_payload) {
-      size_t prev_len = Curl_bufq_len(&ws->sendbuf);
-      result = ws_enc_write_payload(&ws->enc, data,
-                                    buffer + ws->sendbuf_payload,
-                                    buflen - ws->sendbuf_payload,
-                                    &ws->sendbuf, &n);
-      if(result && (result != CURLE_AGAIN))
-        goto out;
-      ws->sendbuf_payload += Curl_bufq_len(&ws->sendbuf) - prev_len;
-      if(!ws->sendbuf_payload) {
-        result = CURLE_AGAIN;
-        goto out;
-      }
-    }
-
-    /* flush, blocking when in callback */
-    result = ws_flush(data, ws, Curl_is_in_callback(data));
-    if(!result && ws->sendbuf_payload > 0) {
-      if(sent)
-        *sent += ws->sendbuf_payload;
-      buffer += ws->sendbuf_payload;
-      buflen -= ws->sendbuf_payload;
-      ws->sendbuf_payload = 0;
-    }
-    else if(result == CURLE_AGAIN) {
-      if(ws->sendbuf_payload > Curl_bufq_len(&ws->sendbuf)) {
-        /* blocked, part of payload bytes remain, report length
-         * that we managed to send. */
-        size_t flushed = (ws->sendbuf_payload - Curl_bufq_len(&ws->sendbuf));
-        if(sent)
-          *sent += flushed;
-        ws->sendbuf_payload -= flushed;
-        result = CURLE_OK;
-        goto out;
-      }
-      else {
-        /* blocked before sending headers or 1st payload byte. We cannot report
-         * OK on 0-length send (caller counts only payload) and EAGAIN */
-        CURL_TRC_WS(data, "EAGAIN flushing sendbuf, payload_encoded: %zu/%zu",
-                    ws->sendbuf_payload, buflen);
-        DEBUGASSERT(!sent || *sent == 0);
-        result = CURLE_AGAIN;
-        goto out;
-      }
-    }
-    else
-      goto out;  /* real error sending the data */
-  }
+  /* Not RAW mode, we do the frame encoding */
+  result = ws_enc_send(data, ws, buffer, buflen, fragsize, flags, pnsent);
 
 out:
   CURL_TRC_WS(data, "curl_ws_send(len=%zu, fragsize=%" FMT_OFF_T
               ", flags=%x, raw=%d) -> %d, %zu",
               buflen, fragsize, flags, data->set.ws_raw_mode, result,
-              sent ? *sent : 0);
+              *pnsent);
   return result;
 }
 
@@ -1760,7 +1875,8 @@ CURL_EXTERN CURLcode curl_ws_start_frame(CURL *d,
     goto out;
   }
 
-  result = ws_enc_write_head(data, &ws->enc, flags, frame_len, &ws->sendbuf);
+  result = ws_enc_write_head(data, ws, &ws->enc, flags, frame_len,
+                             &ws->sendbuf);
   if(result)
     CURL_TRC_WS(data, "curl_start_frame(), error  adding frame head %d",
                 result);