fix(esp_http_server): let the WS control handler own the protocol reply

This commit is contained in:
Ashish Sharma
2026-09-04 16:24:38 +08:00
parent 731ef25e37
commit 8a01f77647
9 changed files with 116 additions and 69 deletions
@@ -96,10 +96,10 @@ This example registers a dedicated control-frame handler on the `/ws` endpoint (
```c
.handle_ws_control_frames = true,
.ws_control_handler = ws_control_frame_handler, // observes PING/PONG/CLOSE
.ws_control_handler = ws_control_frame_handler, // handles PING/PONG/CLOSE
```
The handler only observes the frames (this example logs them); the server still sends the protocol replies (PONG for PING, CLOSE for CLOSE) itself. Send the text message `Ping` to the server to watch the full heartbeat round trip: the server sends a PING and the client's PONG response is logged by the control-frame handler.
Control frames then go to `ws_control_frame_handler()` instead of the data handler, so `echo_handler()` only deals with TEXT/BINARY frames. The server receives the control frame body for the handler but does not reply, so the handler sends the protocol reply itself (a PONG echoing the payload for a PING, an empty CLOSE for a CLOSE). Send the text message `Ping` to the server to watch the full heartbeat round trip: the server sends a PING and the client's PONG response is logged by the control-frame handler.
### Hardware Required
@@ -25,9 +25,9 @@ menu "Example Configuration"
default y
help
Enable this option to register a dedicated handler for WebSocket
control frames (PING, PONG, CLOSE) on the /ws endpoint. The handler
only observes the frames (e.g. for heartbeat tracking or logging);
the server still sends the protocol replies (PONG for PING, CLOSE
for CLOSE) itself.
control frames (PING, PONG, CLOSE) on the /ws endpoint, keeping them
out of the data handler. The server receives the frame body for the
handler, which then sends the protocol replies itself (PONG for
PING, CLOSE for CLOSE).
endmenu
@@ -141,31 +141,39 @@ static esp_err_t ws_post_handshake_cb(httpd_req_t *req)
#ifdef CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER
/*
* Dedicated control-frame handler: observes PING/PONG/CLOSE frames without
* receiving them in the data handler. The frame is read-only and owned by the
* server, which sends the protocol reply (PONG for PING, CLOSE for CLOSE)
* itself after this handler returns.
* Dedicated control-frame handler: keeps PING/PONG/CLOSE out of the data handler
* below, which therefore only deals with TEXT/BINARY frames.
*
* The server has already received the frame body (no allocation needed here) but
* does not reply, so this handler owns the protocol reply: a PONG echoing the
* payload for a PING, an empty CLOSE for a CLOSE, nothing for a PONG. The frame
* is owned by the server and is reused in place for the reply; it must not be
* freed, retained, or grown past the received length.
*
* Type "Ping" in the client to see the full heartbeat round trip: the server
* sends a PING and the client's PONG response lands here.
*/
static esp_err_t ws_control_frame_handler(httpd_req_t *req, const httpd_ws_frame_t *frame)
static esp_err_t ws_control_frame_handler(httpd_req_t *req, httpd_ws_frame_t *frame)
{
switch (frame->type) {
case HTTPD_WS_TYPE_PING:
ESP_LOGI(TAG, "Control frame: PING (len %d), server replies PONG", frame->len);
break;
ESP_LOGI(TAG, "Control frame: PING (len %d), replying PONG", frame->len);
frame->type = HTTPD_WS_TYPE_PONG;
return httpd_ws_send_frame(req, frame);
case HTTPD_WS_TYPE_PONG:
/* Reply to our own PING; nothing to send back (RFC 6455, section 5.5.3) */
ESP_LOGI(TAG, "Control frame: PONG, heartbeat alive");
break;
return ESP_OK;
case HTTPD_WS_TYPE_CLOSE:
ESP_LOGI(TAG, "Control frame: CLOSE (len %d), server replies CLOSE", frame->len);
break;
ESP_LOGI(TAG, "Control frame: CLOSE (len %d), replying CLOSE", frame->len);
frame->len = 0;
frame->payload = NULL;
return httpd_ws_send_frame(req, frame);
default:
ESP_LOGI(TAG, "Control frame: type %d", frame->type);
break;
/* Reserved control opcode: fail the connection (RFC 6455, section 5.2) */
ESP_LOGW(TAG, "Control frame: reserved opcode %d, closing connection", frame->type);
return ESP_FAIL;
}
return ESP_OK;
}
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
@@ -312,8 +320,8 @@ static const httpd_uri_t ws = {
.user_ctx = NULL,
.is_websocket = true,
#ifdef CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER
/* Route control frames to the dedicated handler; the server still
* sends the protocol replies itself. */
/* Route control frames to the dedicated handler, which owns the
* protocol replies; echo_handler() then only sees data frames. */
.handle_ws_control_frames = true,
.ws_control_handler = ws_control_frame_handler,
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */