[api] Mark send_message nodiscard so refused frames are never silent (#18293)

This commit is contained in:
J. Nick Koston
2026-08-11 22:23:08 -05:00
committed by GitHub
parent 37eae9b466
commit a2feff8f68
7 changed files with 113 additions and 37 deletions
+54 -7
View File
@@ -89,6 +89,13 @@ static_assert(ESPHOME_DEVICE_NAME_MAX_LEN <= 31, "Update max_data_length for nam
static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto"); static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto");
static const char *const TAG = "api.connection"; static const char *const TAG = "api.connection";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
void log_dropped_message(const char *tag, int line, const LogString *what) {
esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, line, ESPHOME_LOG_FORMAT("%s dropped, TCP buffer full"),
LOG_STR_ARG(what));
}
#endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
static const int CAMERA_STOP_STREAM = 5000; static const int CAMERA_STOP_STREAM = 5000;
#endif #endif
@@ -1536,7 +1543,13 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
#endif #endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) #if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
if (!this->send_message(msg)) {
// V: fires per decoded frame with no subscription gate, so a warning
// would flood the congested link it reports on.
ESP_LOGV(TAG, "IR/RF event dropped, TCP buffer full");
}
}
#endif #endif
#ifdef USE_SERIAL_PROXY #ifdef USE_SERIAL_PROXY
@@ -1578,7 +1591,9 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
SerialProxyGetModemPinsResponse resp{}; SerialProxyGetModemPinsResponse resp{};
resp.instance = msg.instance; resp.instance = msg.instance;
resp.line_states = proxies[msg.instance]->get_modem_pins(); resp.line_states = proxies[msg.instance]->get_modem_pins();
this->send_message(resp); if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
} }
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
@@ -1610,7 +1625,9 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
resp.status = enums::SERIAL_PROXY_STATUS_ERROR; resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
break; break;
} }
this->send_message(resp); if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
break; break;
} }
default: default:
@@ -1619,7 +1636,11 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
} }
} }
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); } void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
if (!this->send_message(msg)) {
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
}
}
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
@@ -1750,7 +1771,9 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
// Acknowledge the hello so the client can read the server name, then request // Acknowledge the hello so the client can read the server name, then request
// disconnect with the reason. Authentication is intentionally not completed. // disconnect with the reason. Authentication is intentionally not completed.
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Provisioning closed; rejecting connection")); this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Provisioning closed; rejecting connection"));
this->send_message(resp); if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Hello response");
}
DisconnectRequest req; DisconnectRequest req;
req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED; req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
return this->send_message(req); return this->send_message(req);
@@ -2039,7 +2062,9 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success
resp.call_id = call_id; resp.call_id = call_id;
resp.success = success; resp.success = success;
resp.error_message = error_message; resp.error_message = error_message;
this->send_message(resp); if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Action response");
}
} }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
@@ -2050,12 +2075,34 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success
resp.error_message = error_message; resp.error_message = error_message;
resp.response_data = response_data; resp.response_data = response_data;
resp.response_data_len = response_data_len; resp.response_data_len = response_data_len;
this->send_message(resp); if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Action response");
}
} }
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
bool APIConnection::send_homeassistant_action(const HomeassistantActionRequest &call) {
if (!this->flags_.service_call_subscription)
return false;
if (!this->send_message(call)) {
API_LOG_MSG_DROPPED(TAG, "Action request");
}
return true;
}
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_HOMEASSISTANT_TIME
void APIConnection::send_time_request() {
GetTimeRequest req;
if (!this->send_message(req)) {
API_LOG_MSG_DROPPED(TAG, "Time request");
}
}
#endif // USE_HOMEASSISTANT_TIME
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) { void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
+16 -11
View File
@@ -25,6 +25,7 @@
#include "esphome/components/esp8266/crash_handler.h" #include "esphome/components/esp8266/crash_handler.h"
#endif #endif
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h" #include "esphome/core/string_ref.h"
#include <functional> #include <functional>
@@ -40,6 +41,16 @@ namespace esphome::api {
// Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h. // Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h.
class APIServer; class APIServer;
// One shared flash string for every refused-frame warning: send_message()
// fails as soon as the TCP buffer is full, and each caller only pays for its
// short name. The guard drops the helper and its arguments below WARN.
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
void log_dropped_message(const char *tag, int line, const LogString *what);
#define API_LOG_MSG_DROPPED(tag, what) esphome::api::log_dropped_message(tag, __LINE__, LOG_STR(what))
#else
#define API_LOG_MSG_DROPPED(tag, what)
#endif
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending // Maximum number of entities to process in a single batch during initial state/info sending
@@ -169,12 +180,7 @@ class APIConnection final : public APIServerConnectionBase {
// Returns whether this client has subscribed to Home Assistant actions; the message // Returns whether this client has subscribed to Home Assistant actions; the message
// is only handed to the send path when subscribed. A true return does not guarantee // is only handed to the send path when subscribed. A true return does not guarantee
// delivery - it lets the caller warn when no connected client has the subscription. // delivery - it lets the caller warn when no connected client has the subscription.
bool send_homeassistant_action(const HomeassistantActionRequest &call) { bool send_homeassistant_action(const HomeassistantActionRequest &call);
if (!this->flags_.service_call_subscription)
return false;
this->send_message(call);
return true;
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg); void on_homeassistant_action_response(const HomeassistantActionResponse &msg);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -198,10 +204,7 @@ class APIConnection final : public APIServerConnectionBase {
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void send_time_request() { void send_time_request();
GetTimeRequest req;
this->send_message(req);
}
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -337,7 +340,9 @@ class APIConnection final : public APIServerConnectionBase {
// Function pointer type for type-erased size calculation // Function pointer type for type-erased size calculation
using CalculateSizeFn = uint32_t (*)(const void *); using CalculateSizeFn = uint32_t (*)(const void *);
template<typename T> bool send_message(const T &msg) { /// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
/// have no silent failures: every caller must handle (or log) a refusal.
template<typename T> [[nodiscard]] bool send_message(const T &msg) {
if constexpr (T::ESTIMATED_SIZE == 0) { if constexpr (T::ESTIMATED_SIZE == 0) {
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg); return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
} else { } else {
+11 -4
View File
@@ -123,7 +123,9 @@ void APIServer::setup() {
// Best-effort: if the send buffer is full the reason is dropped, but the // Best-effort: if the send buffer is full the reason is dropped, but the
// client still learns the window is closed when it reconnects (rejected at // client still learns the window is closed when it reconnects (rejected at
// hello) or via the socket close. // hello) or via the socket close.
c->send_message(req); if (!c->send_message(req)) {
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
}
} }
}); });
} }
@@ -394,8 +396,11 @@ void APIServer::on_update(update::UpdateEntity *obj) {
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is // We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients // very infrequent and small, we simply send it to all clients
for (auto &c : this->active_clients()) for (auto &c : this->active_clients()) {
c->send_message(msg); if (!c->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "Home ID notification");
}
}
} }
#endif #endif
@@ -576,7 +581,9 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
for (auto &c : this->active_clients()) { for (auto &c : this->active_clients()) {
DisconnectRequest req; DisconnectRequest req;
c->send_message(req); if (!c->send_message(req)) {
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
}
} }
}); });
} }
@@ -150,8 +150,12 @@ void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handl
} }
#endif #endif
void BluetoothProxy::log_advertisement_flush_() { void BluetoothProxy::log_advertisement_flush_(bool sent) {
ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); if (sent) {
ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len);
} else {
ESP_LOGV(TAG, "Batch of %u BLE advertisements dropped, TCP buffer full", this->response_.advertisements_len);
}
} }
void BluetoothProxy::dump_config() { void BluetoothProxy::dump_config() {
@@ -234,16 +234,15 @@ class BluetoothProxy final : public Component {
void flush_pending_advertisements_() { void flush_pending_advertisements_() {
if (this->response_.advertisements_len == 0) if (this->response_.advertisements_len == 0)
return; return;
// The one deliberately ignored result: advertisements are perishable and // Perishable and the highest-frequency send here: a drop only reports at
// this is the highest-frequency send here, so reporting each drop would be // V, anything louder would be the flood the batch pacing exists to avoid.
// the flood the batch pacing exists to avoid. [[maybe_unused]] bool sent = this->api_connection_->send_message(this->response_);
this->api_connection_->send_message(this->response_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->log_advertisement_flush_(); this->log_advertisement_flush_(sent);
#endif #endif
this->response_.advertisements_len = 0; this->response_.advertisements_len = 0;
} }
void log_advertisement_flush_(); void log_advertisement_flush_(bool sent);
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
BluetoothConnection *get_connection_(uint64_t address, bool reserve); BluetoothConnection *get_connection_(uint64_t address, bool reserve);
@@ -248,7 +248,9 @@ void VoiceAssistant::stream_api_audio_() {
msg.data2_len = available2; msg.data2_len = available2;
} }
this->api_client_->send_message(msg); if (!this->api_client_->send_message(msg)) {
ESP_LOGV(TAG, "Audio frame dropped, TCP buffer full");
}
this->audio_source_->consume(available); this->audio_source_->consume(available);
if (this->audio_source2_ != nullptr) { if (this->audio_source2_ != nullptr) {
@@ -477,7 +479,9 @@ void VoiceAssistant::loop() {
api::VoiceAssistantAnnounceFinished msg; api::VoiceAssistantAnnounceFinished msg;
msg.success = true; msg.success = true;
this->api_client_->send_message(msg); if (!this->api_client_->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "Announce-finished");
}
break; break;
} }
} }
@@ -741,7 +745,9 @@ void VoiceAssistant::signal_stop_() {
ESP_LOGD(TAG, "Signaling stop"); ESP_LOGD(TAG, "Signaling stop");
api::VoiceAssistantRequest msg; api::VoiceAssistantRequest msg;
msg.start = false; msg.start = false;
this->api_client_->send_message(msg); if (!this->api_client_->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "Stop request");
}
} }
void VoiceAssistant::start_playback_timeout_() { void VoiceAssistant::start_playback_timeout_() {
@@ -753,7 +759,9 @@ void VoiceAssistant::start_playback_timeout_() {
return; return;
api::VoiceAssistantAnnounceFinished msg; api::VoiceAssistantAnnounceFinished msg;
msg.success = true; msg.success = true;
this->api_client_->send_message(msg); if (!this->api_client_->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "Announce-finished");
}
}); });
} }
@@ -166,7 +166,9 @@ void ZWaveProxy::process_uart_slow_() {
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
} }
this->api_connection_->send_message(this->outgoing_proto_msg_); if (!this->api_connection_->send_message(this->outgoing_proto_msg_)) {
ESP_LOGV(TAG, "Frame dropped, TCP buffer full");
}
} }
} }
} while (this->available()); } while (this->available());
@@ -328,7 +330,9 @@ void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) {
msg.data_len = this->home_id_.size(); msg.data_len = this->home_id_.size();
if (conn != nullptr) { if (conn != nullptr) {
// Send to specific connection // Send to specific connection
conn->send_message(msg); if (!conn->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "Home ID notification");
}
} else if (api::global_api_server != nullptr) { } else if (api::global_api_server != nullptr) {
// We could add code to manage a second subscription type, but, since this message is // We could add code to manage a second subscription type, but, since this message is
// very infrequent and small, we simply send it to all clients // very infrequent and small, we simply send it to all clients
@@ -483,7 +487,9 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
this->buffer_[0] = byte; this->buffer_[0] = byte;
this->outgoing_proto_msg_.data = this->buffer_.data(); this->outgoing_proto_msg_.data = this->buffer_.data();
this->outgoing_proto_msg_.data_len = 1; this->outgoing_proto_msg_.data_len = 1;
this->api_connection_->send_message(this->outgoing_proto_msg_); if (!this->api_connection_->send_message(this->outgoing_proto_msg_)) {
ESP_LOGV(TAG, "Frame dropped, TCP buffer full");
}
} }
} }