diff --git a/components/esp_http_client/esp_http_client.c b/components/esp_http_client/esp_http_client.c index 997dda34bce..9457dff691e 100644 --- a/components/esp_http_client/esp_http_client.c +++ b/components/esp_http_client/esp_http_client.c @@ -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) { diff --git a/components/esp_http_client/include/esp_http_client.h b/components/esp_http_client/include/esp_http_client.h index 272107fc4ed..b143b97d875 100644 --- a/components/esp_http_client/include/esp_http_client.h +++ b/components/esp_http_client/include/esp_http_client.h @@ -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 (\\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 * diff --git a/docs/en/api-reference/protocols/esp_http_client.rst b/docs/en/api-reference/protocols/esp_http_client.rst index 5510148a669..96426721a9a 100644 --- a/docs/en/api-reference/protocols/esp_http_client.rst +++ b/docs/en/api-reference/protocols/esp_http_client.rst @@ -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. diff --git a/docs/zh_CN/api-reference/protocols/esp_http_client.rst b/docs/zh_CN/api-reference/protocols/esp_http_client.rst index 06448005d21..d2938c90e77 100644 --- a/docs/zh_CN/api-reference/protocols/esp_http_client.rst +++ b/docs/zh_CN/api-reference/protocols/esp_http_client.rst @@ -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`:关闭连接。 diff --git a/examples/protocols/esp_http_client/main/esp_http_client_example.c b/examples/protocols/esp_http_client/main/esp_http_client_example.c index 3ddfdb2ba5b..658317b7ae9 100644 --- a/examples/protocols/esp_http_client/main/esp_http_client_example.c +++ b/examples/protocols/esp_http_client/main/esp_http_client_example.c @@ -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 diff --git a/examples/protocols/esp_http_client/pytest_esp_http_client.py b/examples/protocols/esp_http_client/pytest_esp_http_client.py index 9d6fbbd5f73..aeae1bce517 100644 --- a/examples/protocols/esp_http_client/pytest_esp_http_client.py +++ b/examples/protocols/esp_http_client/pytest_esp_http_client.py @@ -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')