test(esp_http_server): cover WebSocket control-frame handler

This commit is contained in:
Ashish Sharma
2026-07-09 14:31:18 +08:00
parent 0d664083d0
commit 5bd8050985
5 changed files with 300 additions and 11 deletions

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