From 79775ae42b9732b48c7d07301857036e93a0c39d Mon Sep 17 00:00:00 2001 From: huyl Date: Mon, 28 Sep 2026 19:14:42 +0800 Subject: [PATCH] fix(http): parse ws frames wrapped in the handshake TCP segment A websocket client may send its first frame in the same TCP segment as the upgrade request. On the server, that frame is the unparsed tail of the recv buffer handed to HttpHandler::FeedRecvData: the MESSAGE_COMPLETE hook runs synchronously inside http-parser and emits the 101, but the http parser stops at the request boundary and returns nfeed < len, so on_recv saw 'http parse error: success' and closed the healthy upgraded io -- the channel died on the client's first message. Track the completed upgrade with a ws_upgraded flag and, when set, feed the remaining bytes of the same buffer to ws_parser instead of failing. Also seed last_recv_pong_time/last_send_ping_time at upgrade time: both start at 0, so the first heartbeat tick would see recv < send and close the channel before any ping/pong round-trip happened. Co-Authored-By: Claude Code --- http/server/HttpHandler.cpp | 34 +++++++++++++++++++++++++++++++++- http/server/HttpHandler.h | 3 +++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index 8a1d4ac35..76608c94d 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -36,6 +36,7 @@ HttpHandler::HttpHandler(hio_t* io) : proxy_connected(0), forward_proxy(0), reverse_proxy(0), + ws_upgraded(0), ip{'\0'}, port(0), pid(0), @@ -203,8 +204,21 @@ bool HttpHandler::SwitchWebSocket() { if(!io) return false; protocol = WEBSOCKET; + // A client may wrap its first ws frame (or a close) in the SAME TCP + // segment as the handshake request; onMessageComplete() then returns with + // state==SEND_DONE and FeedRecvData's HTTP_V1 branch would feed those + // bytes to the http parser (consuming 0 -> "http parse error" -> the io is + // closed and the channel never works). Seed both pong bookkeeping + // variables so the first heartbeat tick passes, and remember the + // upgrade is complete so the HTTP_V1 recv path can hand the leftover + // bytes to ws_parser instead. + ws_upgraded = true; ws_parser = std::make_shared(); ws_channel = std::make_shared(io, WS_SERVER); + // Base timestamps: both are 0 at upgrade, so initialize them NOW; the + // first heartbeat tick must see last_recv_pong_time >= last_send_ping_time + // or it closes a healthy channel before any round-trip happened. + last_recv_pong_time = last_send_ping_time = gethrtime_us(); ws_parser->onMessage = [this](int opcode, const std::string& msg){ ws_channel->opcode = (enum ws_opcode)opcode; switch(opcode) { @@ -808,7 +822,25 @@ int HttpHandler::FeedRecvData(const char* data, size_t len) { Reset(); } nfeed = parser->FeedRecvData(data, len); - // printf("FeedRecvData %d=>%d\n", (int)len, nfeed); + // A websocket upgrade may complete INSIDE that parse (the + // http_cb MESSAGE_COMPLETE hook runs synchronously, sends the 101 and + // flips protocol to WEBSOCKET). If the client wrapped its first ws + // frame in the SAME TCP segment as the handshake, that frame is the + // unparsed tail of this very buffer: hand it to ws_parser instead of + // reporting an http parse error below (which would close a perfectly + // healthy upgraded channel). + if (ws_upgraded && protocol == HttpHandler::WEBSOCKET + && nfeed >= 0 && (size_t)nfeed < len) { + const char* ws_data = data + nfeed; + size_t ws_len = len - nfeed; + int nws = ws_parser->FeedRecvData(ws_data, ws_len); + if (nws != (int)ws_len) { + hloge("[%s:%d] websocket parse error!", ip, port); + error = ERR_PARSE; + return -1; + } + nfeed = (int)len; + } if (nfeed != len) { hloge("[%s:%d] http parse error: %s", ip, port, parser->StrError(parser->GetError())); error = ERR_PARSE; diff --git a/http/server/HttpHandler.h b/http/server/HttpHandler.h index aaa45b34b..8bf346595 100644 --- a/http/server/HttpHandler.h +++ b/http/server/HttpHandler.h @@ -40,6 +40,9 @@ class HttpHandler { unsigned proxy_connected :1; unsigned forward_proxy :1; unsigned reverse_proxy :1; + // set by SwitchWebSocket: after the 101 the handler parses ws frames + // (see FeedRecvData), even for bytes riding the handshake's TCP segment + unsigned ws_upgraded :1; // peeraddr char ip[64];