feat(esp_http_client): detect oversized headers in tx buffer while sending request

This commit is contained in:
Ashish Sharma
2026-07-07 18:22:08 +08:00
parent 76f1151d72
commit 1041b69131
10 changed files with 243 additions and 4 deletions
@@ -1,3 +1,5 @@
idf_component_register(SRC_DIRS "."
PRIV_INCLUDE_DIRS "."
PRIV_REQUIRES esp_http_client test_utils unity)
"../../lib/include"
PRIV_REQUIRES esp_http_client tcp_transport test_utils unity
WHOLE_ARCHIVE)
@@ -1,16 +1,18 @@
/*
* SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <esp_system.h>
#include <esp_http_client.h>
#include "unity.h"
#include "test_utils.h"
#include "sdkconfig.h"
#define HOST "httpbin.org"
#define USERNAME "user"
@@ -221,6 +223,108 @@ TEST_CASE("esp_http_client_close() and cleanup() should not dispatch duplicate d
TEST_ASSERT_LESS_OR_EQUAL(1, disconnect_event_count);
}
#ifdef CONFIG_ESP_HTTP_CLIENT_STRICT_HEADER_BUFFER
#if CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
#include "esp_transport.h"
/*
* Minimal stub transport for the Case B test below. write() always reports
* success so the test can isolate the strict-header check from the real
* transport's failure path.
*/
static int stub_transport_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms) { return 0; }
static int stub_transport_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms) { return len; }
static int stub_transport_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms) { return 0; }
static int stub_transport_close(esp_transport_handle_t t) { return 0; }
static int stub_transport_destroy(esp_transport_handle_t t) { return 0; }
static int stub_transport_poll(esp_transport_handle_t t, int timeout_ms) { return 1; }
#endif // CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
TEST_CASE("esp_http_client_request_send fails when a header exceeds tx buffer", "[ESP HTTP CLIENT]")
{
/*
* The "first header too big" path: with a small buffer_size_tx and an
* oversized header, http_header_generate_string() returns 0 and the
* helper exits the write loop without ever touching the transport.
* The after-loop strict check must surface ESP_ERR_HTTP_HEADER_TOO_LONG
* instead of silently completing as ESP_OK.
*/
esp_http_client_config_t config = {
.url = "http://example.com/",
.buffer_size_tx = 128,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
/* Drop the default User-Agent and Host headers so the oversized header
* is the very first entry. That keeps us on the Case A path (helper
* returns 0 with wlen=0) so the after-loop strict check fires before
* esp_transport_write is ever attempted — no network needed. */
esp_http_client_set_header(client, "User-Agent", NULL);
esp_http_client_set_header(client, "Host", NULL);
char huge_value[200];
memset(huge_value, 'A', sizeof(huge_value) - 1);
huge_value[sizeof(huge_value) - 1] = '\0';
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_set_header(client, "X-Huge", huge_value));
esp_err_t err = esp_http_client_request_send(client, 0);
TEST_ASSERT_EQUAL(ESP_ERR_HTTP_HEADER_TOO_LONG, err);
esp_http_client_cleanup(client);
}
#if CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
TEST_CASE("esp_http_client_request_send fails when an oversized header is mid-list", "[ESP HTTP CLIENT]")
{
/*
* Case B: the small header at index 0 paginates, the oversized header
* at index 1 cannot fit on its own. A stub transport accepts the first
* chunk so the loop runs a second iteration, where the strict check
* fires. The stub can only succeed, so an ESP_ERR_HTTP_HEADER_TOO_LONG
* return here is unambiguously the strict check (not transport failure).
*/
esp_transport_handle_t stub = esp_transport_init();
TEST_ASSERT_NOT_NULL(stub);
TEST_ASSERT_EQUAL(ESP_OK, esp_transport_set_func(stub,
stub_transport_connect,
stub_transport_read,
stub_transport_write,
stub_transport_close,
stub_transport_poll,
stub_transport_poll,
stub_transport_destroy));
esp_http_client_config_t config = {
.url = "http://example.com/",
.buffer_size_tx = 128,
.transport = stub,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
/* Clear defaults and set: one small header (fits), one huge header
* (alone too big for the buffer). */
esp_http_client_set_header(client, "User-Agent", NULL);
esp_http_client_set_header(client, "Host", NULL);
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_set_header(client, "K1", "V1"));
char huge_value[200];
memset(huge_value, 'A', sizeof(huge_value) - 1);
huge_value[sizeof(huge_value) - 1] = '\0';
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_set_header(client, "X-Huge", huge_value));
esp_err_t err = esp_http_client_request_send(client, 0);
TEST_ASSERT_EQUAL(ESP_ERR_HTTP_HEADER_TOO_LONG, err);
esp_http_client_cleanup(client);
esp_transport_destroy(stub);
}
#endif // CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
#endif // CONFIG_ESP_HTTP_CLIENT_STRICT_HEADER_BUFFER
void app_main(void)
{
unity_run_menu();
@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Unit tests for http_header_generate_string() — cover the empty list,
* all-headers-fit and pagination paths.
*/
#include <stdlib.h>
#include <string.h>
#include "unity.h"
#include "http_header.h"
TEST_CASE("http_header_generate_string: empty list returns 0 without touching buffer", "[http_header]")
{
http_header_handle_t hdr = http_header_init();
TEST_ASSERT_NOT_NULL(hdr);
char buf[64] = "untouched";
int buf_len = sizeof(buf);
int ret = http_header_generate_string(hdr, 0, buf, &buf_len);
TEST_ASSERT_EQUAL_INT(0, ret);
TEST_ASSERT_EQUAL_INT((int)sizeof(buf), buf_len);
TEST_ASSERT_EQUAL_STRING("untouched", buf);
http_header_destroy(hdr);
}
TEST_CASE("http_header_generate_string: all headers fit in one call", "[http_header]")
{
http_header_handle_t hdr = http_header_init();
TEST_ASSERT_NOT_NULL(hdr);
TEST_ASSERT_EQUAL(ESP_OK, http_header_set(hdr, "K1", "V1"));
TEST_ASSERT_EQUAL(ESP_OK, http_header_set(hdr, "K2", "V2"));
char buf[64] = {0};
int buf_len = sizeof(buf);
int ret = http_header_generate_string(hdr, 0, buf, &buf_len);
const char *expected = "K1: V1\r\nK2: V2\r\n\r\n";
TEST_ASSERT_EQUAL_INT(2, ret);
TEST_ASSERT_EQUAL_STRING(expected, buf);
TEST_ASSERT_EQUAL_INT((int)strlen(expected), buf_len);
http_header_destroy(hdr);
}
TEST_CASE("http_header_generate_string: pagination across two calls", "[http_header]")
{
http_header_handle_t hdr = http_header_init();
TEST_ASSERT_NOT_NULL(hdr);
TEST_ASSERT_EQUAL(ESP_OK, http_header_set(hdr, "K1", "V1"));
TEST_ASSERT_EQUAL(ESP_OK, http_header_set(hdr, "K2", "V2"));
/* 12-byte buffer fits one 8-byte header, but not both headers + terminator. */
char buf[12] = {0};
int buf_len = sizeof(buf);
int ret = http_header_generate_string(hdr, 0, buf, &buf_len);
TEST_ASSERT_EQUAL_INT(1, ret);
TEST_ASSERT_EQUAL_STRING("K1: V1\r\n", buf);
TEST_ASSERT_EQUAL_INT((int)strlen("K1: V1\r\n"), buf_len);
memset(buf, 0, sizeof(buf));
buf_len = sizeof(buf);
ret = http_header_generate_string(hdr, 1, buf, &buf_len);
TEST_ASSERT_EQUAL_INT(2, ret);
TEST_ASSERT_EQUAL_STRING("K2: V2\r\n\r\n", buf);
TEST_ASSERT_EQUAL_INT((int)strlen("K2: V2\r\n\r\n"), buf_len);
http_header_destroy(hdr);
}
@@ -6,7 +6,7 @@ from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('config', ['default'], indirect=['config'])
@idf_parametrize('config', ['default', 'strict_header'], indirect=['config'])
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
def test_esp_http_client(dut: Dut) -> None:
dut.run_all_single_board_cases()
@@ -0,0 +1,2 @@
CONFIG_ESP_HTTP_CLIENT_STRICT_HEADER_BUFFER=y
CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT=y