// HTTP Client Component — synchronous GET / POST wrapper around esp_http_client. // See tdd/http_client_component.md for design rationale. #include "http_client.hpp" #include #include #include #include "esp_http_client.h" #include "esp_log.h" #include "types.hpp" internal const char *kTagHttpClient = "HTTP_CLIENT"; // ── Internal receive buffer ───────────────────────────────────────────────── struct http_receive_buffer_t { char *buf; size_t len; size_t capacity; }; // ── Event handler ─────────────────────────────────────────────────────────── internal esp_err_t http_event_handler(esp_http_client_event_t *evt) { auto *recv = (http_receive_buffer_t *)evt->user_data; switch (evt->event_id) { case HTTP_EVENT_ON_DATA: { if (recv == nullptr) { break; } size_t new_len = recv->len + evt->data_len; // Grow buffer if needed if (new_len > recv->capacity) { // Double or fit, whichever is larger size_t new_cap = recv->capacity * 2; if (new_cap < new_len) { new_cap = new_len; } // +1 for potential null terminator char *new_buf = (char *)realloc(recv->buf, new_cap + 1); if (new_buf == nullptr) { ESP_LOGE(kTagHttpClient, "realloc failed (%zu bytes)", new_cap + 1); return ESP_ERR_NO_MEM; } recv->buf = new_buf; recv->capacity = new_cap; } memcpy(recv->buf + recv->len, evt->data, evt->data_len); recv->len = new_len; break; } case HTTP_EVENT_ERROR: ESP_LOGE(kTagHttpClient, "HTTP_EVENT_ERROR"); break; case HTTP_EVENT_ON_CONNECTED: case HTTP_EVENT_HEADERS_SENT: case HTTP_EVENT_ON_HEADER: case HTTP_EVENT_ON_FINISH: case HTTP_EVENT_DISCONNECTED: case HTTP_EVENT_REDIRECT: default: break; } return ESP_OK; } // ── Shared perform helper ─────────────────────────────────────────────────── // Performs an HTTP request, accumulates body into recv buffer. // On success, recv->buf contains the raw response body (not null-terminated). // Caller is responsible for cleaning up recv->buf on both success and failure. internal esp_err_t http_perform(const char *url, esp_http_client_method_t method, const char *post_data, size_t post_data_len, const char *content_type, http_receive_buffer_t *recv, int *out_status_code) { *out_status_code = 0; esp_http_client_config_t config = {}; config.url = url; config.method = method; config.event_handler = http_event_handler; config.user_data = recv; config.timeout_ms = 10000; config.buffer_size = 1024; config.buffer_size_tx = 1024; esp_http_client_handle_t client = esp_http_client_init(&config); if (client == nullptr) { ESP_LOGE(kTagHttpClient, "Failed to init HTTP client for %s", url); return ESP_FAIL; } // Set POST body if provided if (post_data != nullptr && post_data_len > 0) { esp_http_client_set_post_field(client, post_data, (int)post_data_len); } // Set Content-Type header if provided if (content_type != nullptr) { esp_http_client_set_header(client, "Content-Type", content_type); } ESP_LOGI(kTagHttpClient, "%s %s", method == HTTP_METHOD_POST ? "POST" : "GET", url); esp_err_t err = esp_http_client_perform(client); if (err != ESP_OK) { ESP_LOGE(kTagHttpClient, "HTTP request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } *out_status_code = esp_http_client_get_status_code(client); int64_t content_length = esp_http_client_get_content_length(client); if (*out_status_code >= 200 && *out_status_code < 300) { ESP_LOGI(kTagHttpClient, "Response: %d, %zu bytes received", *out_status_code, recv->len); } else { ESP_LOGW(kTagHttpClient, "Response: %d (content-length: %lld)", *out_status_code, content_length); } esp_http_client_cleanup(client); return ESP_OK; } // ── Public API ────────────────────────────────────────────────────────────── char *http_build_url(const char *host, uint16_t port, const char *path) { // "http://" (7) + host + ":" (1) + port (max 5) + path + null size_t host_len = strlen(host); size_t path_len = path ? strlen(path) : 0; size_t url_len = 7 + host_len + 1 + 5 + path_len + 1; char *url = (char *)malloc(url_len); if (url == nullptr) { ESP_LOGE(kTagHttpClient, "Failed to allocate URL buffer"); return nullptr; } snprintf(url, url_len, "http://%s:%u%s", host, port, path ? path : ""); return url; } esp_err_t http_get_text(const char *url, http_text_response_t *out) { memset(out, 0, sizeof(*out)); http_receive_buffer_t recv = {}; int status_code = 0; esp_err_t err = http_perform(url, HTTP_METHOD_GET, nullptr, 0, nullptr, &recv, &status_code); if (err != ESP_OK) { free(recv.buf); return err; } // Null-terminate the text body if (recv.buf != nullptr) { recv.buf[recv.len] = '\0'; // Safe: we always allocate capacity + 1 } out->status_code = status_code; out->body = recv.buf; out->body_len = recv.len; return ESP_OK; } esp_err_t http_get_binary(const char *url, http_binary_response_t *out) { memset(out, 0, sizeof(*out)); http_receive_buffer_t recv = {}; int status_code = 0; esp_err_t err = http_perform(url, HTTP_METHOD_GET, nullptr, 0, nullptr, &recv, &status_code); if (err != ESP_OK) { free(recv.buf); return err; } out->status_code = status_code; out->data = (uint8_t *)recv.buf; out->data_len = recv.len; return ESP_OK; } esp_err_t http_post_json(const char *url, const char *json_body, http_text_response_t *out) { memset(out, 0, sizeof(*out)); http_receive_buffer_t recv = {}; int status_code = 0; size_t body_len = json_body ? strlen(json_body) : 0; esp_err_t err = http_perform(url, HTTP_METHOD_POST, json_body, body_len, "application/json", &recv, &status_code); if (err != ESP_OK) { free(recv.buf); return err; } // Null-terminate the text body if (recv.buf != nullptr) { recv.buf[recv.len] = '\0'; } out->status_code = status_code; out->body = recv.buf; out->body_len = recv.len; return ESP_OK; }