Merge branch 'feat/ws-control-frame-handler' into 'master'

feat(esp_http_server): separate control frame handler for WS server

Closes IDFGH-17509

See merge request espressif/esp-idf!50616
This commit is contained in:
Mahavir Jain
2026-08-19 14:20:16 +05:30
12 changed files with 532 additions and 59 deletions

View File

@@ -431,6 +431,13 @@ typedef struct httpd_req {
bool ignore_sess_ctx_changes;
} httpd_req_t;
#if CONFIG_HTTPD_WS_SUPPORT
/* Forward declaration of the WebSocket frame type. The full definition appears
* later in this header; only a pointer to it is needed here so that httpd_uri_t
* can carry an optional WebSocket control-frame handler. */
typedef struct httpd_ws_frame httpd_ws_frame_t;
#endif
/**
* @brief Structure for URI handler
*/
@@ -481,6 +488,23 @@ typedef struct httpd_uri {
*/
esp_err_t (*ws_post_handshake_cb)(httpd_req_t *req);
#endif /* CONFIG_HTTPD_WS_POST_HANDSHAKE_CB_SUPPORT */
/**
* Optional dedicated handler for WebSocket control frames (PING, PONG, CLOSE).
*
* Only takes effect when handle_ws_control_frames is true. When set, control
* frames are delivered to this handler instead of the data handler. The server
* has already received the frame (passed via the read-only frame argument), and
* after this handler returns the server performs the protocol reply itself
* (PONG for PING, CLOSE for CLOSE). The frame and its payload are owned by the
* server and are only valid for the duration of the call; the handler must not
* free or retain them. If left NULL, control frames continue to be delivered to
* the data handler (unchanged behavior).
*
* Placed at the end of the struct to keep positional initialization of existing
* fields backward compatible.
*/
esp_err_t (*ws_control_handler)(httpd_req_t *req, const httpd_ws_frame_t *frame);
#endif /* CONFIG_HTTPD_WS_SUPPORT */
} httpd_uri_t;

View File

@@ -89,6 +89,7 @@ struct sock_db {
bool ws_close; /*!< Set to true to close the socket later (when WS Close frame received) */
esp_err_t (*ws_handler)(httpd_req_t *r); /*!< WebSocket handler, leave to null if it's not WebSocket */
bool ws_control_frames; /*!< WebSocket flag indicating that control frames should be passed to user handlers */
esp_err_t (*ws_control_handler)(httpd_req_t *r, const httpd_ws_frame_t *frame); /*!< Dedicated WebSocket control-frame handler, NULL if not used */
void *ws_user_ctx; /*!< Pointer to user context data which will be available to handler for websocket*/
#endif
};
@@ -553,6 +554,61 @@ esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *suppor
*/
esp_err_t httpd_ws_get_frame_type(httpd_req_t *req);
#ifdef CONFIG_HTTPD_WS_SUPPORT
/* Control frames carry at most 125 bytes of payload (RFC 6455 §5.5). These values
* match the historical auto-reply path: a 128-byte receive buffer, and a 126-byte
* cap passed to httpd_ws_recv_frame(). */
#define HTTPD_WS_CTRL_FRAME_BUF_LEN 128
#define HTTPD_WS_CTRL_FRAME_MAX_LEN 126
/**
* @brief Receive the body of a WebSocket control frame into a caller buffer.
*
* @note The opcode/FIN must already have been decoded by httpd_ws_get_frame_type().
* On success @p frame describes the received (unmasked) control frame with
* @p frame->payload pointing into @p buf.
*
* @param[in] req WebSocket request
* @param[out] frame Frame descriptor to populate
* @param[in] buf Caller-owned buffer of at least HTTPD_WS_CTRL_FRAME_BUF_LEN bytes
* @param[in] max_len Maximum payload length to accept
* @return
* - ESP_OK : Frame received
* - ESP_ERR_INVALID_STATE : Frame could not be fully received
*/
esp_err_t httpd_ws_recv_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame, uint8_t *buf, size_t max_len);
/**
* @brief Send the protocol reply for a received WebSocket control frame.
*
* @note PING is answered with a PONG echoing the payload; CLOSE is answered with
* an empty CLOSE; all other control frames (e.g. PONG) require no reply.
*
* @param[in] req WebSocket request
* @param[in] frame Control frame previously received (modified in place)
* @return
* - ESP_OK : Reply sent (or none needed)
* - others : Socket send failure
*/
esp_err_t httpd_ws_reply_to_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame);
/**
* @brief Handle an incoming WebSocket control frame via the dedicated control handler.
*
* @note Used only when a ws_control_handler is registered (handle_ws_control_frames
* must be true). The server receives the frame body, passes a read-only view
* to the control handler, then performs the protocol reply itself. If the
* control handler returns an error, the reply is still sent and the error is
* propagated so the caller closes the socket.
*
* @param[in] req WebSocket request
* @return
* - ESP_OK : Control frame handled and replied
* - others : Control handler error, or frame could not be received/replied
*/
esp_err_t httpd_ws_handle_control_frame(httpd_req_t *req);
#endif /* CONFIG_HTTPD_WS_SUPPORT */
/**
* @brief Trigger an httpd session close externally
*

View File

@@ -851,17 +851,21 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
ESP_LOGD(TAG, LOG_FMT("Received PONG frame"));
}
/* Call handler if it's a non-control frame, a PONG frame,
* or if handler requests control frames as well.
* PONG must be dispatched so that:
* 1. User code that sends PINGs can track responses (heartbeat)
* 2. The PONG frame bytes are consumed from the socket via
* httpd_ws_recv_frame(), preventing TCP stream misalignment */
if (ret == ESP_OK &&
(ra->ws_type < HTTPD_WS_TYPE_CLOSE ||
ra->ws_type == HTTPD_WS_TYPE_PONG ||
sd->ws_control_frames)) {
ret = sd->ws_handler(r);
/* Dispatch the frame:
* - Control frames (CLOSE/PING/PONG) go to the dedicated control handler
* when one is registered; the server then sends the protocol reply.
* - Otherwise dispatch to the data handler for non-control frames, PONG
* frames, or when the handler opted in to receiving control frames.
* PONG must be dispatched so that user heartbeat code can track it and
* so its bytes are consumed from the socket (avoiding stream misalignment). */
if (ret == ESP_OK) {
if (ra->ws_type >= HTTPD_WS_TYPE_CLOSE && sd->ws_control_handler != NULL) {
ret = httpd_ws_handle_control_frame(r);
} else if (ra->ws_type < HTTPD_WS_TYPE_CLOSE ||
ra->ws_type == HTTPD_WS_TYPE_PONG ||
sd->ws_control_frames) {
ret = sd->ws_handler(r);
}
}
if (ret != ESP_OK) {

View File

@@ -176,6 +176,7 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle,
#ifdef CONFIG_HTTPD_WS_SUPPORT
hd->hd_calls[i]->is_websocket = uri_handler->is_websocket;
hd->hd_calls[i]->handle_ws_control_frames = uri_handler->handle_ws_control_frames;
hd->hd_calls[i]->ws_control_handler = uri_handler->ws_control_handler;
if (uri_handler->supported_subprotocol) {
hd->hd_calls[i]->supported_subprotocol = strdup(uri_handler->supported_subprotocol);
if (hd->hd_calls[i]->supported_subprotocol == NULL) {
@@ -353,6 +354,7 @@ esp_err_t httpd_uri(struct httpd_data *hd)
aux->sd->ws_handshake_done = true;
aux->sd->ws_handler = uri->handler;
aux->sd->ws_control_frames = uri->handle_ws_control_frames;
aux->sd->ws_control_handler = uri->handle_ws_control_frames ? uri->ws_control_handler : NULL;
aux->sd->ws_user_ctx = uri->user_ctx;
#ifdef CONFIG_HTTPD_WS_POST_HANDSHAKE_CB_SUPPORT

View File

@@ -689,6 +689,71 @@ esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t
return ESP_OK;
}
esp_err_t httpd_ws_recv_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame,
uint8_t *buf, size_t max_len)
{
/* The opcode/FIN were already decoded by httpd_ws_get_frame_type(); a zeroed
* frame (len == 0) makes httpd_ws_recv_frame() read the remaining header and
* payload. Control payloads are <= 125 bytes (RFC 6455 §5.5). */
memset(frame, 0, sizeof(*frame));
frame->payload = buf;
if (httpd_ws_recv_frame(req, frame, max_len) != ESP_OK) {
ESP_LOGD(TAG, LOG_FMT("Cannot receive the full control frame"));
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
esp_err_t httpd_ws_reply_to_control_frame(httpd_req_t *req, httpd_ws_frame_t *frame)
{
switch (frame->type) {
case HTTPD_WS_TYPE_PING:
/* Reply to a PING with a PONG echoing the payload (RFC 6455 §5.5.2/5.5.3) */
ESP_LOGD(TAG, LOG_FMT("Got a WS PING frame, Replying PONG..."));
frame->type = HTTPD_WS_TYPE_PONG;
return httpd_ws_send_frame(req, frame);
case HTTPD_WS_TYPE_CLOSE:
/* Reply to a CLOSE with an empty CLOSE (RFC 6455 §5.5.1) */
ESP_LOGD(TAG, LOG_FMT("Got a WS CLOSE frame, Replying CLOSE..."));
frame->len = 0;
frame->payload = NULL;
return httpd_ws_send_frame(req, frame);
default:
/* PONG and any other control frame require no reply */
return ESP_OK;
}
}
esp_err_t httpd_ws_handle_control_frame(httpd_req_t *req)
{
struct httpd_req_aux *aux = req->aux;
if (aux == NULL || aux->sd == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct sock_db *sd = aux->sd;
/* The server receives the control-frame body itself. Oversized or malformed
* frames are rejected by the max_len cap (zero-trust on client input). */
httpd_ws_frame_t frame;
uint8_t frame_buf[HTTPD_WS_CTRL_FRAME_BUF_LEN] = { 0 };
esp_err_t ret = httpd_ws_recv_control_frame(req, &frame, frame_buf, HTTPD_WS_CTRL_FRAME_MAX_LEN);
if (ret != ESP_OK) {
return ret;
}
/* Notify the user's control handler with a read-only view of the frame. */
esp_err_t handler_ret = ESP_OK;
if (sd->ws_control_handler != NULL) {
handler_ret = sd->ws_control_handler(req, &frame);
}
/* The server always performs the protocol reply (PONG for PING, CLOSE for
* CLOSE). If the handler failed, still reply, then propagate the error so the
* caller closes the socket. */
esp_err_t reply_ret = httpd_ws_reply_to_control_frame(req, &frame);
return (handler_ret != ESP_OK) ? handler_ret : reply_ret;
}
esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
{
esp_err_t ret = httpd_ws_check_req(req);
@@ -756,48 +821,21 @@ esp_err_t httpd_ws_get_frame_type(httpd_req_t *req)
}
#endif /* CONFIG_HTTPD_WS_STRICTER_RFC6455 */
/* If userspace requests control frames, do not deal with the control frames */
/* If userspace requests control frames, do not deal with the control frames here */
if (!sd->ws_control_frames) {
ESP_LOGD(TAG, LOG_FMT("Handler not requests control frames"));
/* Reply to PING. For PONG and CLOSE, it will be handled elsewhere. */
if (aux->ws_type == HTTPD_WS_TYPE_PING) {
ESP_LOGD(TAG, LOG_FMT("Got a WS PING frame, Replying PONG..."));
/* Read the rest of the PING frame, for PONG to reply back. */
/* Please refer to RFC6455 Section 5.5.2 for more details */
/* Auto-handle PING and CLOSE: receive the body, then send the reply. PONG is
* deliberately not consumed here; it is dispatched to the data handler elsewhere. */
if (aux->ws_type == HTTPD_WS_TYPE_PING || aux->ws_type == HTTPD_WS_TYPE_CLOSE) {
httpd_ws_frame_t frame;
uint8_t frame_buf[128] = { 0 };
memset(&frame, 0, sizeof(httpd_ws_frame_t));
frame.payload = frame_buf;
if (httpd_ws_recv_frame(req, &frame, 126) != ESP_OK) {
ESP_LOGD(TAG, LOG_FMT("Cannot receive the full PING frame"));
return ESP_ERR_INVALID_STATE;
uint8_t frame_buf[HTTPD_WS_CTRL_FRAME_BUF_LEN] = { 0 };
esp_err_t recv_frame_ret = httpd_ws_recv_control_frame(req, &frame, frame_buf,
HTTPD_WS_CTRL_FRAME_MAX_LEN);
if (recv_frame_ret != ESP_OK) {
return recv_frame_ret;
}
/* Now turn the frame to PONG */
frame.type = HTTPD_WS_TYPE_PONG;
return httpd_ws_send_frame(req, &frame);
} else if (aux->ws_type == HTTPD_WS_TYPE_CLOSE) {
ESP_LOGD(TAG, LOG_FMT("Got a WS CLOSE frame, Replying CLOSE..."));
/* Read the rest of the CLOSE frame and response */
/* Please refer to RFC6455 Section 5.5.1 for more details */
httpd_ws_frame_t frame;
uint8_t frame_buf[128] = { 0 };
memset(&frame, 0, sizeof(httpd_ws_frame_t));
frame.payload = frame_buf;
if (httpd_ws_recv_frame(req, &frame, 126) != ESP_OK) {
ESP_LOGD(TAG, LOG_FMT("Cannot receive the full CLOSE frame"));
return ESP_ERR_INVALID_STATE;
}
frame.len = 0;
frame.type = HTTPD_WS_TYPE_CLOSE;
frame.payload = NULL;
return httpd_ws_send_frame(req, &frame);
return httpd_ws_reply_to_control_frame(req, &frame);
}
}
return ESP_OK;

View File

@@ -561,6 +561,98 @@ TEST_CASE("httpd_queue_work fast-fails on ctrl mbox saturation", "[HTTP SERVER]"
}
#ifdef CONFIG_HTTPD_WS_SUPPORT
/* ------------------------------------------------------------------------- *
* White-box fixtures for the dedicated control-frame handler.
*
* These tests drive httpd_req_new() directly against a fake sock_db, feeding a
* crafted (zero-masked) WebSocket frame through a recv_fn override and capturing
* the server's reply through a send_fn override. No TCP/IP is involved, so they
* run identically on hardware and under QEMU.
* ------------------------------------------------------------------------- */
static int s_ws_data_handler_calls;
static int s_ws_control_handler_calls;
static httpd_ws_type_t s_ws_control_seen_type;
static size_t s_ws_control_seen_len;
static esp_err_t s_ws_control_ret;
static const uint8_t *s_ws_recv_data;
static size_t s_ws_recv_len;
static size_t s_ws_recv_off;
static uint8_t s_ws_sent[128];
static size_t s_ws_sent_len;
/* recv_fn override: serve the crafted frame byte stream, one chunk per call. */
static int ws_feed_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags)
{
(void)hd; (void)sockfd; (void)flags;
size_t remaining = s_ws_recv_len - s_ws_recv_off;
if (remaining == 0) {
return HTTPD_SOCK_ERR_FAIL;
}
size_t n = (buf_len < remaining) ? buf_len : remaining;
memcpy(buf, s_ws_recv_data + s_ws_recv_off, n);
s_ws_recv_off += n;
return (int)n;
}
/* send_fn override: capture whatever the server sends back (the protocol reply). */
static int ws_capture_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags)
{
(void)hd; (void)sockfd; (void)flags;
for (size_t i = 0; i < buf_len && s_ws_sent_len < sizeof(s_ws_sent); i++) {
s_ws_sent[s_ws_sent_len++] = (uint8_t)buf[i];
}
return (int)buf_len;
}
static esp_err_t ws_data_handler_spy(httpd_req_t *req)
{
(void)req;
s_ws_data_handler_calls++;
return ESP_OK;
}
static esp_err_t ws_control_handler_spy(httpd_req_t *req, const httpd_ws_frame_t *frame)
{
(void)req;
s_ws_control_handler_calls++;
s_ws_control_seen_type = frame->type;
s_ws_control_seen_len = frame->len;
return s_ws_control_ret;
}
/* Wire a fake session/server and reset all fixtures around a single frame. */
static void ws_unit_ctx_init(struct httpd_data *hd, struct sock_db *session,
const uint8_t *frame, size_t frame_len)
{
s_ws_data_handler_calls = 0;
s_ws_control_handler_calls = 0;
s_ws_control_seen_type = HTTPD_WS_TYPE_CONTINUE;
s_ws_control_seen_len = 0;
s_ws_control_ret = ESP_OK;
s_ws_recv_data = frame;
s_ws_recv_len = frame_len;
s_ws_recv_off = 0;
s_ws_sent_len = 0;
memset(s_ws_sent, 0, sizeof(s_ws_sent));
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = 1; /* keep any session enumeration in bounds */
hd->config = config;
hd->hd_sd = session; /* non-NULL so httpd_sess_get() finds the reply target */
hd->hd_req_aux.resp_hdrs = calloc(config.max_resp_headers, sizeof(*hd->hd_req_aux.resp_hdrs));
TEST_ASSERT_NOT_NULL(hd->hd_req_aux.resp_hdrs);
session->fd = 123;
session->handle = (httpd_handle_t) hd;
session->recv_fn = ws_feed_recv;
session->send_fn = ws_capture_send;
session->ws_handshake_done = true;
session->ws_handler = ws_data_handler_spy;
session->ws_close = false;
}
TEST_CASE("WS recv failure marks close without dispatching handler", "[HTTP SERVER][websocket]")
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
@@ -944,6 +1036,119 @@ TEST_CASE("WS send uses 16-bit length encoding for exactly 65535-byte payload",
TEST_ASSERT_EQUAL(sizeof(expected_header), ws_send_capture_ctx.len);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_header, ws_send_capture_ctx.data, sizeof(expected_header));
}
TEST_CASE("WS control handler receives PING and server replies PONG", "[HTTP SERVER][websocket]")
{
/* Masked (zero-key) PING carrying a 2-byte payload "Hi". */
static const uint8_t ping_frame[] = { 0x89, 0x82, 0x00, 0x00, 0x00, 0x00, 'H', 'i' };
struct httpd_data hd = {0};
struct sock_db session = {0};
ws_unit_ctx_init(&hd, &session, ping_frame, sizeof(ping_frame));
session.ws_control_frames = true;
session.ws_control_handler = ws_control_handler_spy;
esp_err_t ret = httpd_req_new(&hd, &session);
TEST_ASSERT_EQUAL(ESP_OK, ret);
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_PING, s_ws_control_seen_type);
TEST_ASSERT_EQUAL(2, s_ws_control_seen_len);
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls); /* control frame must not reach data handler */
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
TEST_ASSERT_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* FIN | PONG */
TEST_ASSERT_FALSE(session.ws_close);
free(hd.hd_req_aux.resp_hdrs);
}
TEST_CASE("WS control handler receives CLOSE and server replies CLOSE", "[HTTP SERVER][websocket]")
{
static const uint8_t close_frame[] = { 0x88, 0x80, 0x00, 0x00, 0x00, 0x00 };
struct httpd_data hd = {0};
struct sock_db session = {0};
ws_unit_ctx_init(&hd, &session, close_frame, sizeof(close_frame));
session.ws_control_frames = true;
session.ws_control_handler = ws_control_handler_spy;
esp_err_t ret = httpd_req_new(&hd, &session);
TEST_ASSERT_EQUAL(ESP_OK, ret);
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_CLOSE, s_ws_control_seen_type);
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls);
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
TEST_ASSERT_EQUAL_HEX8(0x88, s_ws_sent[0]); /* FIN | CLOSE */
TEST_ASSERT_TRUE(session.ws_close); /* server marked the session for close */
free(hd.hd_req_aux.resp_hdrs);
}
TEST_CASE("WS control handler receives PONG with no reply", "[HTTP SERVER][websocket]")
{
static const uint8_t pong_frame[] = { 0x8A, 0x80, 0x00, 0x00, 0x00, 0x00 };
struct httpd_data hd = {0};
struct sock_db session = {0};
ws_unit_ctx_init(&hd, &session, pong_frame, sizeof(pong_frame));
session.ws_control_frames = true;
session.ws_control_handler = ws_control_handler_spy;
esp_err_t ret = httpd_req_new(&hd, &session);
TEST_ASSERT_EQUAL(ESP_OK, ret);
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
TEST_ASSERT_EQUAL(HTTPD_WS_TYPE_PONG, s_ws_control_seen_type);
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls);
TEST_ASSERT_EQUAL(0, s_ws_sent_len); /* a PONG is never answered */
TEST_ASSERT_FALSE(session.ws_close);
free(hd.hd_req_aux.resp_hdrs);
}
TEST_CASE("WS without control handler auto-replies PING (backward compatible)", "[HTTP SERVER][websocket]")
{
/* Mode 1: no control handler, flag off -> server must still auto-reply PONG and
* must not dispatch the PING to the data handler (unchanged legacy behavior). */
static const uint8_t ping_frame[] = { 0x89, 0x80, 0x00, 0x00, 0x00, 0x00 };
struct httpd_data hd = {0};
struct sock_db session = {0};
ws_unit_ctx_init(&hd, &session, ping_frame, sizeof(ping_frame));
session.ws_control_frames = false;
session.ws_control_handler = NULL;
esp_err_t ret = httpd_req_new(&hd, &session);
TEST_ASSERT_EQUAL(ESP_OK, ret);
TEST_ASSERT_EQUAL(0, s_ws_control_handler_calls);
TEST_ASSERT_EQUAL(0, s_ws_data_handler_calls);
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
TEST_ASSERT_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* auto PONG */
free(hd.hd_req_aux.resp_hdrs);
}
TEST_CASE("WS control handler error still replies then closes socket", "[HTTP SERVER][websocket]")
{
/* A PING is used so ws_close stays false and cleanup does not touch the fake
* control socket. The handler fails, but the server must still send the PONG,
* and httpd_req_new() must propagate the error so the caller closes the socket. */
static const uint8_t ping_frame[] = { 0x89, 0x80, 0x00, 0x00, 0x00, 0x00 };
struct httpd_data hd = {0};
struct sock_db session = {0};
ws_unit_ctx_init(&hd, &session, ping_frame, sizeof(ping_frame));
session.ws_control_frames = true;
session.ws_control_handler = ws_control_handler_spy;
s_ws_control_ret = ESP_FAIL;
esp_err_t ret = httpd_req_new(&hd, &session);
TEST_ASSERT_EQUAL(ESP_FAIL, ret); /* error propagated to caller */
TEST_ASSERT_EQUAL(1, s_ws_control_handler_calls);
TEST_ASSERT_GREATER_THAN(0, s_ws_sent_len);
TEST_ASSERT_EQUAL_HEX8(0x8A, s_ws_sent[0]); /* reply sent despite handler error */
free(hd.hd_req_aux.resp_hdrs);
}
#endif /* CONFIG_HTTPD_WS_SUPPORT */
/********* URL query / header pointer-accessor tests *********

View File

@@ -139,6 +139,36 @@ To use the WebSocket post-handshake callback, you must enable :menuitem:`CONFIG_
httpd_register_uri_handler(server, &ws);
WebSocket Control Frame Handler
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, the server replies to WebSocket control frames automatically — a PING frame is answered with a PONG and a CLOSE frame is answered with a CLOSE — without involving the application. Setting ``handle_ws_control_frames`` to true in :cpp:type:`httpd_uri_t` disables this behavior and delivers control frames to the data handler, which then becomes responsible for receiving the frame and sending the protocol replies itself.
The ``ws_control_handler`` callback provides a middle ground: when it is set (and ``handle_ws_control_frames`` is true), control frames (PING, PONG, CLOSE) are delivered to this dedicated handler instead of the data handler, while the server still receives the frame body and performs the protocol replies itself after the handler returns. This is useful for observing heartbeats (tracking PONG responses) or logging the close reason without re-implementing the reply logic.
The frame passed to the handler is read-only and owned by the server; it is only valid for the duration of the call, so the handler must not free or retain it. If the handler returns an error, the server still sends the protocol reply and then closes the connection.
.. code-block:: c
static esp_err_t ws_control_frame_handler(httpd_req_t *req, const httpd_ws_frame_t *frame)
{
// Observe PING/PONG/CLOSE here (e.g. heartbeat tracking, logging).
// The server sends the protocol reply itself after this returns.
return ESP_OK;
}
// Registering a WebSocket URI handler with a dedicated control-frame handler
static const httpd_uri_t ws = {
.uri = "/ws",
.method = HTTP_GET,
.handler = handler, // Your WebSocket data handler
.user_ctx = NULL,
.is_websocket = true,
.handle_ws_control_frames = true,
.ws_control_handler = ws_control_frame_handler
};
Event Handling
--------------

View File

@@ -139,6 +139,36 @@ WebSocket 握手后回调
httpd_register_uri_handler(server, &ws);
WebSocket 控制帧处理程序
^^^^^^^^^^^^^^^^^^^^^^^^^^
默认情况下,服务器会自动回复 WebSocket 控制帧——收到 PING 帧时回复 PONG 帧,收到 CLOSE 帧时回复 CLOSE 帧——应用程序不会参与该过程。若将 :cpp:type:`httpd_uri_t` 中的 ``handle_ws_control_frames`` 设置为 true则会禁用该行为控制帧将交由数据处理程序处理此时应用程序需要自行接收控制帧并发送协议回复。
``ws_control_handler`` 回调提供了一种折中方案:设置该回调(且 ``handle_ws_control_frames`` 为 true控制帧PING、PONG、CLOSE将交由该专用处理程序处理而不再传递给数据处理程序服务器仍会自行接收帧体并在回调返回后自动发送协议回复。该机制适用于观测心跳跟踪 PONG 响应)或记录连接关闭原因,而无需重新实现回复逻辑。
传递给该回调的帧为只读,且由服务器持有,仅在回调调用期间有效,因此回调中不得释放或保留该帧。如果回调返回错误,服务器仍会发送协议回复,然后关闭连接。
.. code-block:: c
static esp_err_t ws_control_frame_handler(httpd_req_t *req, const httpd_ws_frame_t *frame)
{
// 在此处观测 PING/PONG/CLOSE 帧(例如心跳跟踪、日志记录)
// 回调返回后,服务器会自行发送协议回复
return ESP_OK;
}
// 注册带有专用控制帧处理程序的 WebSocket URI 处理程序
static const httpd_uri_t ws = {
.uri = "/ws",
.method = HTTP_GET,
.handler = handler, // WebSocket 数据处理程序
.user_ctx = NULL,
.is_websocket = true,
.handle_ws_control_frames = true,
.ws_control_handler = ws_control_frame_handler
};
事件处理
--------------

View File

@@ -76,6 +76,19 @@ Each outgoing frame has the FIN flag set by default.
In case an application wants to send fragmented data, it must be done manually by setting the
`fragmented` option and using the `final` flag as described in [RFC6455, section 5.4](https://tools.ietf.org/html/rfc6455#section-5.4).
#### Handling control frames
By default the server replies to control frames (PING, CLOSE) automatically. Setting `handle_ws_control_frames = true` alone routes control frames to the data handler instead, which then has to receive them and send the protocol replies itself.
This example registers a dedicated control-frame handler on the `/ws` endpoint (see `CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER`, enabled by default):
```c
.handle_ws_control_frames = true,
.ws_control_handler = ws_control_frame_handler, // observes 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.
### Hardware Required

View File

@@ -20,4 +20,14 @@ menu "Example Configuration"
In this example, the post-handshake callback is used to send a welcome message
to the client after the handshake is complete.
config EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER
bool "Enable dedicated WebSocket control-frame handler"
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.
endmenu

View File

@@ -139,6 +139,36 @@ static esp_err_t ws_post_handshake_cb(httpd_req_t *req)
}
#endif /* CONFIG_EXAMPLE_ENABLE_WS_POST_HANDSHAKE_CB */
#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.
*
* 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)
{
switch (frame->type) {
case HTTPD_WS_TYPE_PING:
ESP_LOGI(TAG, "Control frame: PING (len %d), server replies PONG", frame->len);
break;
case HTTPD_WS_TYPE_PONG:
ESP_LOGI(TAG, "Control frame: PONG, heartbeat alive");
break;
case HTTPD_WS_TYPE_CLOSE:
ESP_LOGI(TAG, "Control frame: CLOSE (len %d), server replies CLOSE", frame->len);
break;
default:
ESP_LOGI(TAG, "Control frame: type %d", frame->type);
break;
}
return ESP_OK;
}
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
/*
* This handler echos back the received ws data
* and triggers an async send if certain message received
@@ -278,7 +308,13 @@ static const httpd_uri_t ws = {
.method = HTTP_GET,
.handler = echo_handler,
.user_ctx = NULL,
.is_websocket = true
.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. */
.handle_ws_control_frames = true,
.ws_control_handler = ws_control_frame_handler,
#endif /* CONFIG_EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER */
};
static const httpd_uri_t ws_partial = {

View File

@@ -111,6 +111,11 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
got_ip, got_port = _wait_for_server_ready(dut)
# With the dedicated control-frame handler enabled, PING/PONG/CLOSE are
# observed (logged) by the control handler on the DUT, while the server
# still sends the protocol replies itself.
control_handler_enabled = dut.app.sdkconfig.get('EXAMPLE_ENABLE_WS_CONTROL_FRAME_HANDLER') is True
# Start ws server test
with WsClient(got_ip, got_port, uri='ws') as ws:
DATA = 'Espressif'
@@ -122,6 +127,9 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
if expected_opcode == OPCODE_PING:
if opcode != OPCODE_PONG or data != DATA:
raise RuntimeError(f'Failed to receive correct opcode:{opcode} or data:{data}')
if control_handler_enabled:
# The control-frame handler must have observed the client's PING
dut.expect(rf'Control frame: PING \(len {len(DATA)}\), server replies PONG', timeout=10)
continue
dut_data = dut.expect(r'Got packet with message: ([A-Za-z0-9_]*)')[1]
dut_opcode = dut.expect(r'Packet type: ([0-9]*)')[1].decode()
@@ -150,11 +158,17 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
data = data.decode()
if opcode != OPCODE_PING:
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
# Now we should get a pong in response to our ping
opcode, data = ws.read()
data = data.decode()
if opcode != OPCODE_PONG:
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
# The client library auto-replies PONG to the server's PING. With the
# control-frame handler enabled, the DUT observes that PONG in the
# control handler and does not echo it; otherwise the data handler
# echoes the PONG back to the client.
if control_handler_enabled:
dut.expect('Control frame: PONG, heartbeat alive', timeout=10)
else:
opcode, data = ws.read()
data = data.decode()
if opcode != OPCODE_PONG:
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
ws.write(data='Ping', opcode=OPCODE_TEXT)
# Wait for server to receive the message and send a ping
dut.expect(r'Got packet with message: Ping', timeout=10)
@@ -164,11 +178,22 @@ def test_examples_protocol_http_ws_echo_server(dut: Dut) -> None:
data = data.decode()
if opcode != OPCODE_PING:
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
# Now we should get a pong in response to our ping
opcode, data = ws.read()
data = data.decode()
if opcode != OPCODE_PONG:
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
# The client library auto-replies PONG to the server's PING. With the
# control-frame handler enabled, the DUT observes that PONG in the
# control handler and does not echo it; otherwise the data handler
# echoes the PONG back to the client.
if control_handler_enabled:
dut.expect('Control frame: PONG, heartbeat alive', timeout=10)
else:
opcode, data = ws.read()
data = data.decode()
if opcode != OPCODE_PONG:
raise RuntimeError(f'Failed to receive correct opcode:{opcode}')
# Leaving the context closes the client connection: the CLOSE frame must be
# delivered to the control-frame handler (the server still replies CLOSE).
if control_handler_enabled:
dut.expect('Control frame: CLOSE', timeout=10)
@pytest.mark.wifi_router