refactor: route OTA version check through HttpDownloader (#2076)

With both OtaUpdater and HttpDownloader on esp_http_client
(https://github.com/crosspoint-reader/crosspoint-reader/pull/2074,
https://github.com/crosspoint-reader/crosspoint-reader/pull/2075),
checkForUpdate no longer needs its own client and event handler to fetch
the release JSON. It streams the response straight into
ReleaseJsonParser through a new HttpDownloader::fetchUrl(url,
DataCallback) overload, dropping the duplicate esp_http_client setup,
the HTTP_EVENT_ON_DATA handler, and the totalBytesReceived file global.

DataCallback hands body chunks to a callback without buffering. The
naive alternative, collecting the ~32KB JSON into a std::string, aborts
under -fno-exceptions: the growing allocation collides with the TLS
session's heap mid-fetch and operator new calls abort().

The OTA install path stays on esp_https_ota (flash-write streaming),
which has no HttpDownloader equivalent.

HttpDownloader.h must precede the lwip (esp_http_client) headers in
OtaUpdater.cpp, or Arduino/SdFat macros collide with lwip.


Did you use AI tools to help write this code? partial
This commit is contained in:
Jeremy Klein
2026-05-24 21:44:30 -04:00
committed by GitHub
parent 2823a4a2cd
commit 19954aa1b5
3 changed files with 38 additions and 53 deletions
+8
View File
@@ -163,6 +163,14 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent, c
return runGet(url, username, password, sink) == OK;
}
bool HttpDownloader::fetchUrl(const std::string& url, const DataCallback& onData, const std::string& username,
const std::string& password) {
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
Sink sink;
sink.write = onData;
return runGet(url, username, password, sink) == OK;
}
HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& url, const std::string& destPath,
ProgressCallback progress, bool* cancelFlag,
const std::string& username, const std::string& password) {
+9
View File
@@ -12,6 +12,9 @@
class HttpDownloader {
public:
using ProgressCallback = std::function<void(size_t downloaded, size_t total)>;
// Called with each body chunk as it arrives; return false to abort. Lets a
// streaming parser consume the response without buffering the whole body.
using DataCallback = std::function<bool(const uint8_t* data, size_t len)>;
enum DownloadError {
OK = 0,
@@ -29,6 +32,12 @@ class HttpDownloader {
static bool fetchUrl(const std::string& url, Stream& stream, const std::string& username = "",
const std::string& password = "");
/**
* Stream the response body to onData as it arrives, without buffering it.
*/
static bool fetchUrl(const std::string& url, const DataCallback& onData, const std::string& username = "",
const std::string& password = "");
/**
* Download a file to the SD card with optional credentials.
*/
+21 -53
View File
@@ -1,11 +1,20 @@
#include "OtaUpdater.h"
// clang-format off
// HttpDownloader.h pulls Arduino/SdFat, whose macros collide with lwip's
// ip4_addr.h unless seen before esp_http_client (which includes lwip). Pin this
// order; clang-format would otherwise sort the local header last and break the
// build.
#include "HttpDownloader.h"
#include <Logging.h>
#include <ReleaseJsonParser.h>
#include <esp_crt_bundle.h>
#include <esp_http_client.h>
#include <esp_https_ota.h>
#include <esp_wifi.h>
// clang-format on
#include <string>
namespace {
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/latest";
@@ -13,67 +22,26 @@ constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-rea
esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) {
return esp_http_client_set_header(http_client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
}
size_t totalBytesReceived = 0;
esp_err_t event_handler(esp_http_client_event_t* event) {
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
totalBytesReceived += event->data_len;
LOG_DBG("OTA", "HTTP chunk: %d bytes (total: %zu)", event->data_len, totalBytesReceived);
auto* parser = static_cast<ReleaseJsonParser*>(event->user_data);
parser->feed(static_cast<const char*>(event->data), event->data_len);
return ESP_OK;
}
} // namespace
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
esp_err_t esp_err;
ReleaseJsonParser releaseParser;
esp_http_client_config_t client_config = {
.url = latestReleaseUrl,
.event_handler = event_handler,
// 4096 holds the API response headers; the 32KB body streams through the
// parser in chunks so RX needn't be larger. TX only carries our GET.
// Both free before installUpdate, so smaller leaves it less fragmentation.
.buffer_size = 4096,
.buffer_size_tx = 1024,
.user_data = &releaseParser,
.skip_cert_common_name_check = true,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
};
totalBytesReceived = 0;
LOG_DBG("OTA", "Checking for update (current: %s)", CROSSPOINT_VERSION);
esp_http_client_handle_t client_handle = esp_http_client_init(&client_config);
if (!client_handle) {
LOG_ERR("OTA", "HTTP Client Handle Failed");
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
esp_http_client_cleanup(client_handle);
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_http_client_perform(client_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_perform Failed : %s", esp_err_to_name(esp_err));
esp_http_client_cleanup(client_handle);
// Stream the ~32KB release JSON straight into the parser as it arrives.
// Buffering the whole body in a std::string would add a growing allocation
// on top of the TLS session's heap during the fetch; with -fno-exceptions an
// OOM there aborts. fetchUrl handles the verified-https GET, redirects, and
// User-Agent (see HttpDownloader).
ReleaseJsonParser releaseParser;
const bool ok = HttpDownloader::fetchUrl(latestReleaseUrl, [&releaseParser](const uint8_t* data, size_t len) {
releaseParser.feed(reinterpret_cast<const char*>(data), len);
return true;
});
if (!ok) {
LOG_ERR("OTA", "Release check fetch failed");
return HTTP_ERROR;
}
esp_err = esp_http_client_cleanup(client_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_cleanup Failed : %s", esp_err_to_name(esp_err));
return INTERNAL_UPDATE_ERROR;
}
LOG_DBG("OTA", "Response received: %zu bytes total", totalBytesReceived);
LOG_DBG("OTA", "Parser results: tag=%s firmware=%s", releaseParser.foundTag() ? "yes" : "no",
releaseParser.foundFirmware() ? "yes" : "no");