mirror of
https://github.com/espressif/esp-idf.git
synced 2026-05-28 16:46:31 +03:00
fix(esp_http_client): implement chunked transfer encoding in before tranport write
Closes https://github.com/espressif/esp-idf/issues/17685
This commit is contained in:
@@ -1886,8 +1886,8 @@ esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len)
|
||||
|
||||
int esp_http_client_write(esp_http_client_handle_t client, const char *buffer, int len)
|
||||
{
|
||||
if (client->state < HTTP_STATE_REQ_COMPLETE_HEADER) {
|
||||
return ESP_FAIL;
|
||||
if (client == NULL || len < 0 || client->state < HTTP_STATE_REQ_COMPLETE_HEADER || (buffer == NULL && len > 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int wlen = 0, widx = 0;
|
||||
@@ -1904,6 +1904,46 @@ int esp_http_client_write(esp_http_client_handle_t client, const char *buffer, i
|
||||
return widx;
|
||||
}
|
||||
|
||||
int esp_http_client_chunk_write_begin(esp_http_client_handle_t client, const int len)
|
||||
{
|
||||
if (client == NULL || client->state < HTTP_STATE_REQ_COMPLETE_HEADER || len <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char header_buffer[16];
|
||||
int header_len = snprintf(header_buffer, sizeof(header_buffer), "%x\r\n", len);
|
||||
int wlen = esp_transport_write(client->transport, header_buffer, header_len, client->timeout_ms);
|
||||
|
||||
if (wlen < 0 || wlen != header_len) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int esp_http_client_chunk_write_end(esp_http_client_handle_t client, bool last_chunk)
|
||||
{
|
||||
if (client == NULL || client->state < HTTP_STATE_REQ_COMPLETE_HEADER) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Send chunk trailer: \r\n */
|
||||
int wlen = esp_transport_write(client->transport, "\r\n", 2, client->timeout_ms);
|
||||
if (wlen < 0 || wlen != 2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (last_chunk) {
|
||||
/* Send final terminator: 0\r\n\r\n */
|
||||
const char *terminator = "0\r\n\r\n";
|
||||
wlen = esp_transport_write(client->transport, terminator, strlen(terminator), client->timeout_ms);
|
||||
if (wlen < 0 || wlen != strlen(terminator)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
esp_err_t esp_http_client_close(esp_http_client_handle_t client)
|
||||
{
|
||||
if (client->state > HTTP_STATE_INIT) {
|
||||
|
||||
@@ -651,6 +651,10 @@ esp_err_t esp_http_client_delete_all_headers(esp_http_client_handle_t client);
|
||||
*
|
||||
* @param[in] client The esp_http_client handle
|
||||
* @param[in] write_len HTTP Content length need to write to the server
|
||||
* - If write_len >= 0: Sets Content-Length header with the specified value; use esp_http_client_write() for the body.
|
||||
* - If write_len = -1: Enables chunked transfer encoding (Transfer-Encoding: chunked); use
|
||||
* esp_http_client_chunk_write_begin() / esp_http_client_write() / esp_http_client_chunk_write_end() for each chunk.
|
||||
* Pass last_chunk=true in esp_http_client_chunk_write_end() for the final chunk to send the terminator.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
@@ -662,16 +666,56 @@ esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len);
|
||||
/**
|
||||
* @brief This function will write data to the HTTP connection previously opened by esp_http_client_open()
|
||||
*
|
||||
* @param[in] client The esp_http_client handle
|
||||
* @param buffer The buffer
|
||||
* @param[in] len This value must not be larger than the write_len parameter provided to esp_http_client_open()
|
||||
* @param[in] client The esp_http_client handle (must not be NULL)
|
||||
* @param buffer The buffer (may be NULL only if len is 0)
|
||||
* @param[in] len Length of data to write. Value must not be larger than write_len passed to esp_http_client_open()
|
||||
*
|
||||
* @return
|
||||
* - (-1) if any errors
|
||||
* - Length of data written
|
||||
* - Length of data written on success
|
||||
*
|
||||
* @note When esp_http_client_open() was called with write_len = -1 (chunked encoding), wrap each chunk with
|
||||
* esp_http_client_chunk_write_begin() and esp_http_client_chunk_write_end(). Pass last_chunk=true in
|
||||
* esp_http_client_chunk_write_end() for the final chunk.
|
||||
*/
|
||||
int esp_http_client_write(esp_http_client_handle_t client, const char *buffer, int len);
|
||||
|
||||
/**
|
||||
* @brief Begin writing a chunk in chunked transfer encoding mode.
|
||||
*
|
||||
* Sends the chunk header (<hex-size>\\r\\n) per RFC 7230. After this call, use esp_http_client_write()
|
||||
* to send the chunk body data, then call esp_http_client_chunk_write_end() to finish the chunk.
|
||||
* For normal (non-chunked) write operations this API is not used.
|
||||
*
|
||||
* @pre Transfer-Encoding: chunked header must be set and esp_http_client_open() called with write_len = -1.
|
||||
*
|
||||
* @param[in] client The esp_http_client handle (must not be NULL)
|
||||
* @param[in] len Length of the chunk body that will follow (must be > 0)
|
||||
*
|
||||
* @return
|
||||
* - 0 on success
|
||||
* - -1 on failure (NULL client, invalid state, len <= 0, or transport write error)
|
||||
*/
|
||||
int esp_http_client_chunk_write_begin(esp_http_client_handle_t client, const int len);
|
||||
|
||||
/**
|
||||
* @brief End writing a chunk in chunked transfer encoding mode.
|
||||
*
|
||||
* Sends the chunk trailer (\\r\\n) per RFC 7230 to complete a chunk started by esp_http_client_chunk_write_begin().
|
||||
* When last_chunk is true, also sends the final terminator (0\\r\\n\\r\\n) to signal end of chunked body.
|
||||
* For normal (non-chunked) write operations this API is not used.
|
||||
*
|
||||
* @pre A chunk must have been started with esp_http_client_chunk_write_begin().
|
||||
*
|
||||
* @param[in] client The esp_http_client handle (must not be NULL)
|
||||
* @param[in] last_chunk If true, sends the final chunk terminator (0\\r\\n\\r\\n) after the chunk trailer
|
||||
*
|
||||
* @return
|
||||
* - 0 on success
|
||||
* - -1 on failure (NULL client, invalid state, or transport write error)
|
||||
*/
|
||||
int esp_http_client_chunk_write_end(esp_http_client_handle_t client, bool last_chunk);
|
||||
|
||||
/**
|
||||
* @brief This function need to call after esp_http_client_open, it will read from http stream, process all receive headers
|
||||
*
|
||||
|
||||
@@ -81,8 +81,10 @@ Some applications need to open the connection and control the exchange of data a
|
||||
|
||||
* :cpp:func:`esp_http_client_init`: Create a HTTP client handle.
|
||||
* ``esp_http_client_set_*`` or ``esp_http_client_delete_*``: Modify the HTTP connection parameters (optional).
|
||||
* :cpp:func:`esp_http_client_open`: Open the HTTP connection with ``write_len`` parameter (content length that needs to be written to server), set ``write_len=0`` for read-only connection.
|
||||
* :cpp:func:`esp_http_client_open`: Open the HTTP connection with ``write_len`` parameter (content length that needs to be written to server), set ``write_len=0`` for read-only connection and set ``write_len=-1`` for chunked encoded data transfer.
|
||||
* :cpp:func:`esp_http_client_write`: Write data to server with a maximum length equal to ``write_len`` of :cpp:func:`esp_http_client_open` function; no need to call this function for ``write_len=0``.
|
||||
* :cpp:func:`esp_http_client_chunk_write_begin`: Begin a chunk by sending the chunk header (size line) when using chunked transfer encoding (``write_len=-1``).
|
||||
* :cpp:func:`esp_http_client_chunk_write_end`: End a chunk by sending the chunk trailer when using chunked transfer encoding.
|
||||
* :cpp:func:`esp_http_client_fetch_headers`: Read the HTTP Server response headers, after sending the request headers and server data (if any). Returns the ``content-length`` from the server and can be succeeded by :cpp:func:`esp_http_client_get_status_code` for getting the HTTP status of the connection.
|
||||
* :cpp:func:`esp_http_client_read`: Read the HTTP stream.
|
||||
* :cpp:func:`esp_http_client_close`: Close the connection.
|
||||
|
||||
@@ -81,8 +81,10 @@ HTTP 流
|
||||
|
||||
* :cpp:func:`esp_http_client_init`:创建一个 HTTP 客户端句柄。
|
||||
* ``esp_http_client_set_*`` 或 ``esp_http_client_delete_*``:修改 HTTP 连接参数(可选)。
|
||||
* :cpp:func:`esp_http_client_open`:用 ``write_len`` (该参数为需要写入服务器的内容长度)打开 HTTP 连接,设置 ``write_len=0`` 为只读连接。
|
||||
* :cpp:func:`esp_http_client_open`:用 ``write_len`` (该参数为需要写入服务器的内容长度)打开 HTTP 连接,设置 ``write_len=0`` 为只读连接,设置 ``write_len=-1`` 为分块编码数据传输。
|
||||
* :cpp:func:`esp_http_client_write`:向服务器写入数据,最大长度为 :cpp:func:`esp_http_client_open` 函数中的 ``write_len`` 值;配置 ``write_len=0`` 无需调用此函数。
|
||||
* :cpp:func:`esp_http_client_chunk_write_begin`:使用分块传输编码(``write_len=-1``)时,发送分块头(大小行)以开始一个新分块。
|
||||
* :cpp:func:`esp_http_client_chunk_write_end`:使用分块传输编码时,发送分块尾以结束当前分块。
|
||||
* :cpp:func:`esp_http_client_fetch_headers`:在发送完请求头和服务器数据(如有)后,读取 HTTP 服务器的响应头。从服务器返回 ``content-length``,并可以由 :cpp:func:`esp_http_client_get_status_code` 继承,以获取连接的 HTTP 状态。
|
||||
* :cpp:func:`esp_http_client_read`:读取 HTTP 流。
|
||||
* :cpp:func:`esp_http_client_close`:关闭连接。
|
||||
|
||||
@@ -815,6 +815,192 @@ static void http_native_request(void)
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
/*
|
||||
* http_chunked_request() tests chunked transfer encoding support.
|
||||
*
|
||||
* This test verifies that esp_http_client_chunk_write_begin/write/chunk_write_end correctly
|
||||
* format data according to RFC 7230 chunked transfer encoding. It sends multiple chunks
|
||||
* to demonstrate the chunked API usage.
|
||||
*
|
||||
* Test steps:
|
||||
* 1. Set up POST request with chunked encoding (write_len = -1)
|
||||
* 2. Write multiple chunks using chunk_write_begin() / write() / chunk_write_end() per chunk
|
||||
* 3. Pass last_chunk=true in chunk_write_end() on the final chunk to send the terminator
|
||||
* 4. Verify server accepts the request (Status 200)
|
||||
*/
|
||||
static void http_chunked_request(void)
|
||||
{
|
||||
char output_buffer[MAX_HTTP_OUTPUT_BUFFER + 1] = {0};
|
||||
esp_http_client_config_t config = {
|
||||
.url = "http://"CONFIG_EXAMPLE_HTTP_ENDPOINT"/post",
|
||||
.event_handler = _http_event_handler,
|
||||
.user_data = output_buffer,
|
||||
.timeout_ms = 10000,
|
||||
};
|
||||
ESP_LOGI(TAG, "HTTP chunked request test =>");
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
|
||||
// Open with write_len = -1 to enable chunked encoding (sets Transfer-Encoding: chunked and removes Content-Length automatically)
|
||||
esp_err_t err = esp_http_client_open(client, -1);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send multiple chunks to demonstrate chunked encoding
|
||||
const char *chunk1 = "{\"message\":\"Hello";
|
||||
const char *chunk2 = "\", \"chunks\":";
|
||||
const char *chunk3 = "3}";
|
||||
const char *chunks[] = { chunk1, chunk2, chunk3 };
|
||||
const int num_chunks = sizeof(chunks) / sizeof(chunks[0]);
|
||||
|
||||
for (int i = 0; i < num_chunks; i++) {
|
||||
int len = (int)strlen(chunks[i]);
|
||||
if (esp_http_client_chunk_write_begin(client, len) != 0) {
|
||||
ESP_LOGE(TAG, "Chunk write begin failed for chunk %d", i + 1);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
int wlen = esp_http_client_write(client, chunks[i], len);
|
||||
if (wlen != len) {
|
||||
ESP_LOGE(TAG, "Write failed for chunk %d (got %d, expected %d)", i + 1, wlen, len);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
bool is_last = (i == num_chunks - 1);
|
||||
if (esp_http_client_chunk_write_end(client, is_last) != 0) {
|
||||
ESP_LOGE(TAG, "Chunk write end failed for chunk %d", i + 1);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch headers and read response
|
||||
int64_t content_length = esp_http_client_fetch_headers(client);
|
||||
if (content_length < 0) {
|
||||
ESP_LOGE(TAG, "HTTP client fetch headers failed");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
int status_code = esp_http_client_get_status_code(client);
|
||||
ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %"PRId64, status_code, content_length);
|
||||
|
||||
if (status_code == 200) {
|
||||
ESP_LOGI(TAG, "Chunked encoding test passed - server accepted the request");
|
||||
// Read response only on success
|
||||
int data_read = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
|
||||
if (data_read >= 0) {
|
||||
ESP_LOGD(TAG, "Response received: %.*s", data_read, output_buffer);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to read response");
|
||||
}
|
||||
} else if (status_code == 400) {
|
||||
ESP_LOGE(TAG, "Chunked encoding test failed - server rejected malformed request");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Chunked encoding test returned unexpected status code: %d", status_code);
|
||||
}
|
||||
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
/*
|
||||
* http_chunked_request_async() – chunked transfer encoding in async (non-blocking) mode.
|
||||
* Same flow as http_chunked_request() but with is_async = true. Retry esp_http_client_write()
|
||||
* with remaining data until the full body is written.
|
||||
*/
|
||||
static void http_chunked_request_async(void)
|
||||
{
|
||||
char output_buffer[MAX_HTTP_OUTPUT_BUFFER + 1] = {0};
|
||||
esp_http_client_config_t config = {
|
||||
.url = "http://"CONFIG_EXAMPLE_HTTP_ENDPOINT"/post",
|
||||
.event_handler = _http_event_handler,
|
||||
.user_data = output_buffer,
|
||||
.is_async = true,
|
||||
.timeout_ms = 10000,
|
||||
};
|
||||
ESP_LOGI(TAG, "HTTP chunked request (async mode) test =>");
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
|
||||
if (esp_http_client_open(client, -1) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open HTTP connection");
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *body = "{\"message\":\"Hello, async chunked encoding!\"}";
|
||||
int body_len = (int)strlen(body);
|
||||
int wlen;
|
||||
|
||||
// Send chunk header
|
||||
if (esp_http_client_chunk_write_begin(client, body_len) != 0) {
|
||||
ESP_LOGE(TAG, "Chunk write begin failed");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send chunk body with async retry
|
||||
int written = 0;
|
||||
while (written < body_len) {
|
||||
wlen = esp_http_client_write(client, body + written, body_len - written);
|
||||
if (wlen < 0) {
|
||||
ESP_LOGE(TAG, "Write failed: %d", wlen);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
written += wlen;
|
||||
if (written < body_len) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
// Send chunk trailer + final terminator
|
||||
if (esp_http_client_chunk_write_end(client, true) != 0) {
|
||||
ESP_LOGE(TAG, "Chunk write end failed");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t content_length = esp_http_client_fetch_headers(client);
|
||||
if (content_length < 0) {
|
||||
ESP_LOGE(TAG, "HTTP client fetch headers failed");
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return;
|
||||
}
|
||||
|
||||
int status_code = esp_http_client_get_status_code(client);
|
||||
ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %"PRId64, status_code, content_length);
|
||||
|
||||
if (status_code == 200) {
|
||||
ESP_LOGI(TAG, "Async chunked encoding test passed");
|
||||
int n = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
|
||||
if (n >= 0) {
|
||||
ESP_LOGD(TAG, "Response: %.*s", n, output_buffer);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Async chunked test status: %d", status_code);
|
||||
}
|
||||
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
static void http_partial_download(void)
|
||||
{
|
||||
@@ -891,6 +1077,8 @@ static void http_test_task(void *pvParameters)
|
||||
#endif
|
||||
https_with_invalid_url();
|
||||
http_native_request();
|
||||
http_chunked_request();
|
||||
http_chunked_request_async();
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
http_partial_download();
|
||||
#endif
|
||||
|
||||
@@ -61,6 +61,8 @@ def test_examples_protocol_esp_http_client(dut: Dut) -> None:
|
||||
dut.expect(r'Last esp error code: 0x8001')
|
||||
dut.expect(r'HTTP GET Status = 200, content_length = (\d)')
|
||||
dut.expect(r'HTTP POST Status = 200, content_length = (\d)')
|
||||
dut.expect(r'HTTP POST Status = 200, content_length = (-?\d+)')
|
||||
dut.expect(r'HTTP POST Status = 200, content_length = (-?\d+)')
|
||||
dut.expect(r'HTTP Status = 206, content_length = (\d)')
|
||||
dut.expect(r'HTTP Status = 206, content_length = 10')
|
||||
dut.expect(r'HTTP Status = 206, content_length = 10')
|
||||
@@ -111,6 +113,8 @@ def test_examples_protocol_esp_http_client_dynamic_buffer(dut: Dut) -> None:
|
||||
dut.expect(r'Last esp error code: 0x8001')
|
||||
dut.expect(r'HTTP GET Status = 200, content_length = (\d)')
|
||||
dut.expect(r'HTTP POST Status = 200, content_length = (\d)')
|
||||
dut.expect(r'HTTP POST Status = 200, content_length = (-?\d+)')
|
||||
dut.expect(r'HTTP POST Status = 200, content_length = (-?\d+)')
|
||||
dut.expect(r'HTTP Status = 206, content_length = (\d)')
|
||||
dut.expect(r'HTTP Status = 206, content_length = 10')
|
||||
dut.expect(r'HTTP Status = 206, content_length = 10')
|
||||
|
||||
Reference in New Issue
Block a user