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

This commit is contained in:
Ashish Sharma
2026-08-27 18:37:15 +08:00
parent 731ef25e37
commit 8a01f77647
9 changed files with 116 additions and 69 deletions

View File

@@ -156,19 +156,31 @@ On transmit, the server does not fragment a message automatically. To send fragm
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.
By default, the server replies to WebSocket control frames automatically. A PING frame gets a PONG, and a CLOSE frame gets a CLOSE. The application is not involved.
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.
Set ``handle_ws_control_frames`` to true in :cpp:type:`httpd_uri_t` to turn off the automatic reply. Control frames then go to the data handler. That handler must receive each frame and send the protocol reply itself.
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.
The ``ws_control_handler`` callback keeps control frames out of the data handler. Set it together with ``handle_ws_control_frames``. Control frames (PING, PONG, CLOSE) then go to this dedicated handler, and the data handler only sees data frames.
The server receives the frame body for the handler, so no allocation and no :cpp:func:`httpd_ws_recv_frame` call is needed. The server does not reply. The handler owns the protocol reply. Answer a PING with a PONG that echoes the payload. Answer a CLOSE with a CLOSE. A PONG needs no reply. To send the reply, overwrite ``frame->type`` and pass the frame to :cpp:func:`httpd_ws_send_frame`.
The server owns the frame and its payload. Both are valid only for the duration of the call. The handler must not free them and must not retain them. The handler must not increase ``frame->len`` above the received length, because the payload buffer holds a control frame only. If the handler returns an error, the server sends no reply and closes the connection.
.. code-block:: c
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)
{
// Observe PING/PONG/CLOSE here (e.g. heartbeat tracking, logging).
// The server sends the protocol reply itself after this returns.
return ESP_OK;
switch (frame->type) {
case HTTPD_WS_TYPE_PING:
frame->type = HTTPD_WS_TYPE_PONG; // reuse the frame for the reply
return httpd_ws_send_frame(req, frame);
case HTTPD_WS_TYPE_CLOSE:
frame->len = 0; // an empty CLOSE is a valid reply
frame->payload = NULL;
return httpd_ws_send_frame(req, frame);
default:
return ESP_OK; // a PONG needs no reply
}
}
// Registering a WebSocket URI handler with a dedicated control-frame handler