Merge branch 'fix/esp_http_server-ws-close-and-utf8-strictness' into 'master'

Fix(esp_http_server): ws close and utf8 strictness

See merge request espressif/esp-idf!48908
This commit is contained in:
Mahavir Jain
2026-09-06 15:53:31 +05:30
10 changed files with 770 additions and 99 deletions
@@ -69,9 +69,21 @@ httpd_ws_recv_frame(req, &ws_pkt, MAX_PAYLOAD_LEN);
2) Allocate the size based on the received packet length
3) Call `httpd_ws_recv_frame()` with the allocated buffer
#### Fragmented messages
The WebSocket HTTP server does not support fragmented messages, as [RFC6455, section 5.4](https://tools.ietf.org/html/rfc6455#section-5.4) defines them.
On receive, the server passes each frame to the handler on its own. It does not join the fragments of one message. A handler that gets a message in three fragments sees three separate frames. Use the `final` and `fragmented` fields of `httpd_ws_frame_t` to detect a fragment, and join the payloads in the application.
The server also does not validate the fragment sequence. It accepts a CONTINUE frame that continues no message. It also accepts a new TEXT or BINARY frame while a fragmented message is still open. RFC 6455 requires a close with status code 1002 in both cases.
`CONFIG_HTTPD_WS_STRICTER_RFC6455` validates the UTF-8 of a complete, unfragmented TEXT frame only. It does not validate a TEXT message that arrives in fragments. Join the fragments and call `httpd_ws_validate_utf8()` on the result.
This example echoes each frame as a complete message, so it does not handle fragments either.
#### Handling outgoing data
Please note that the WebSocket HTTP server does not automatically fragment messages.
On transmit, the server does not automatically fragment messages.
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).
@@ -84,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 */
@@ -215,9 +223,11 @@ static esp_err_t echo_handler(httpd_req_t *req)
}
}
ret = httpd_ws_send_frame(req, &ws_pkt);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT || ws_pkt.type == HTTPD_WS_TYPE_BINARY) {
ret = httpd_ws_send_frame(req, &ws_pkt);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret);
}
}
free(buf);
return ret;
@@ -310,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 */