From ba308b6d762e2664c069e8a54b85f7cbcfd51ad2 Mon Sep 17 00:00:00 2001 From: "nilesh.kale" Date: Fri, 30 Jan 2026 15:25:37 +0530 Subject: [PATCH] fix(esp_https_ota): handle ota resumption if server dosent support range requests This commit added check to see if server supports range requets, and fallback to OTA without resumption accordingly. Closes https://github.com/espressif/esp-idf/pull/17960 --- components/esp_http_client/esp_http_client.c | 11 +++ .../esp_http_client/include/esp_http_client.h | 13 +++ components/esp_https_ota/src/esp_https_ota.c | 29 ++++--- .../advanced_https_ota/pytest_advanced_ota.py | 84 ++++++++++++++++++- 4 files changed, 125 insertions(+), 12 deletions(-) diff --git a/components/esp_http_client/esp_http_client.c b/components/esp_http_client/esp_http_client.c index 6823b76bee..299bbf798d 100644 --- a/components/esp_http_client/esp_http_client.c +++ b/components/esp_http_client/esp_http_client.c @@ -1908,6 +1908,17 @@ esp_err_t esp_http_client_close(esp_http_client_handle_t client) return ESP_OK; } +esp_err_t esp_http_client_clear_response_buffer(esp_http_client_handle_t client) +{ + if (client == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (client->response != NULL && client->response->buffer != NULL) { + esp_http_client_cached_buf_cleanup(client->response->buffer); + } + return ESP_OK; +} + esp_err_t esp_http_client_set_post_field(esp_http_client_handle_t client, const char *data, int len) { esp_err_t err = ESP_OK; diff --git a/components/esp_http_client/include/esp_http_client.h b/components/esp_http_client/include/esp_http_client.h index 8bb37af91d..272107fc4e 100644 --- a/components/esp_http_client/include/esp_http_client.h +++ b/components/esp_http_client/include/esp_http_client.h @@ -758,6 +758,19 @@ int64_t esp_http_client_get_content_range(esp_http_client_handle_t client); */ esp_err_t esp_http_client_close(esp_http_client_handle_t client); +/** + * @brief Clear cached response buffer (e.g. data received during fetch headers). + * Use this when reusing the same client handle for a new request after + * closing the connection, so the next request does not see stale data. + * + * @param[in] client The esp_http_client handle + * + * @return + * - ESP_OK + * - ESP_ERR_INVALID_ARG if client is NULL + */ +esp_err_t esp_http_client_clear_response_buffer(esp_http_client_handle_t client); + /** * @brief This function must be the last function to call for an session. * It is the opposite of the esp_http_client_init function and must be called with the same handle as input that a esp_http_client_init call returned. diff --git a/components/esp_https_ota/src/esp_https_ota.c b/components/esp_https_ota/src/esp_https_ota.c index 79e102cb87..39779dba0d 100644 --- a/components/esp_https_ota/src/esp_https_ota.c +++ b/components/esp_https_ota/src/esp_https_ota.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2017-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2017-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -59,6 +59,7 @@ struct esp_https_ota_handle { #endif esp_https_ota_state state; bool bulk_flash_erase; + bool ota_resumption; int max_authorization_retries; #if CONFIG_ESP_HTTPS_OTA_DECRYPT_CB decrypt_cb_t decrypt_cb; @@ -135,12 +136,13 @@ static esp_err_t _http_handle_response_code(esp_https_ota_t *https_ota_handle, i } else if (status_code >= HttpStatus_InternalError) { ESP_LOGE(TAG, "Server error (%d)", status_code); return ESP_FAIL; - } else if (https_ota_handle->binary_file_len > 0 + } else if (https_ota_handle->ota_resumption #if CONFIG_ESP_HTTPS_OTA_ENABLE_PARTIAL_DOWNLOAD && !https_ota_handle->partial_http_download #endif && status_code != HttpStatus_PartialContent) { - ESP_LOGE(TAG, "Requested range header ignored by server"); + ESP_LOGW(TAG, "Server ignored the requested Range header"); + ESP_LOGW(TAG, "OTA resumption requires server with range request support."); return ESP_ERR_HTTP_RANGE_NOT_SATISFIABLE; } @@ -345,6 +347,7 @@ esp_err_t esp_https_ota_begin(const esp_https_ota_config_t *ota_config, esp_http https_ota_handle->partial_http_download = ota_config->partial_http_download; https_ota_handle->max_http_request_size = (ota_config->max_http_request_size == 0) ? DEFAULT_REQUEST_SIZE : ota_config->max_http_request_size; #endif + https_ota_handle->ota_resumption = ota_config->ota_resumption; https_ota_handle->max_authorization_retries = ota_config->http_config->max_authorization_retries; if (https_ota_handle->max_authorization_retries == 0) { @@ -379,12 +382,13 @@ esp_err_t esp_https_ota_begin(const esp_https_ota_config_t *ota_config, esp_http } /* - * If OTA resumption is enabled, set the "Range" header to resume downloading the OTA image - * from the last written byte. For non-partial cases, the range pattern is 'from-'. - * Partial cases ('from-to') are handled separately below based on the remaining data to - * be downloaded and the max_http_request_size. + * If OTA resumption is enabled, always make a range request first to detect if server + * supports range requests. This helps fail early if server doesn't support range requests. + * - If resuming from NVS recovered offset: use 'bytes=offset-' + * - If starting fresh: use 'bytes=0-' + * This applies to both partial and non-partial download cases. */ - if (https_ota_handle->binary_file_len > 0 + if (https_ota_handle->ota_resumption #if CONFIG_ESP_HTTPS_OTA_ENABLE_PARTIAL_DOWNLOAD && !https_ota_handle->partial_http_download #endif @@ -476,9 +480,10 @@ esp_err_t esp_https_ota_begin(const esp_https_ota_config_t *ota_config, esp_http #endif err = _http_connect(https_ota_handle); - if (err == ESP_ERR_HTTP_RANGE_NOT_SATISFIABLE && https_ota_handle->binary_file_len > 0) { - ESP_LOGE(TAG, "OTA resumption failed with err: %d", err); - ESP_LOGI(TAG, "Restarting download from beginning"); + if (err == ESP_ERR_HTTP_RANGE_NOT_SATISFIABLE && https_ota_handle->ota_resumption) { + ESP_LOGE(TAG, "OTA resumption failed with err: %d.", err); + ESP_LOGI(TAG, "Falling back to OTA without resumption and restarting download from beginning"); + https_ota_handle->ota_resumption = false; https_ota_handle->binary_file_len = 0; // If range in request header is not satisfiable, restart download from beginning @@ -497,6 +502,8 @@ esp_err_t esp_https_ota_begin(const esp_https_ota_config_t *ota_config, esp_http free(header_val); } #endif + esp_http_client_close(https_ota_handle->http_client); + esp_http_client_clear_response_buffer(https_ota_handle->http_client); err = _http_connect(https_ota_handle); } diff --git a/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py b/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py index 3caf882225..49f0c6cd52 100644 --- a/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py +++ b/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import http.server import multiprocessing @@ -273,6 +273,88 @@ def test_examples_protocol_advanced_https_ota_example_ota_resumption(dut: Dut) - thread1.terminate() +@pytest.mark.ethernet_ota +@pytest.mark.parametrize('config', ['ota_resumption'], indirect=True) +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_examples_protocol_advanced_https_ota_example_ota_resumption_range_request_support_detection(dut: Dut) -> None: + """ + This test verifies OTA resumption fallback when the server does not support Range requests. + OpenSSL s_server may return 416 Range Not Satisfiable or 200 OK instead of 206 Partial Content. + The OTA should detect this, close the connection, clear the response buffer, and restart + the download from the beginning (ensuring a fresh connection for the retry). + steps: | + 1. join AP/Ethernet + 2. Start OpenSSL s_server (which does not support Range requests) + 3. Fetch OTA image over HTTPS + 4. Restart device mid-download + 5. Resume OTA; fallback to full download from beginning + 6. Verify OTA completes successfully + """ + server_port = 8070 + bin_name = 'advanced_https_ota.bin' + + # Erase NVS partition + dut.serial.erase_partition(NVS_PARTITION) + + # Start openssl s_server which doesn't support Range requests + chunked_server = start_chunked_server(dut.app.binary_path, server_port) + try: + # start test + dut.expect('Loaded app from partition at offset', timeout=30) + + try: + ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=60)[1].decode() + print(f'Connected to AP/Ethernet with IP: {ip_address}') + except pexpect.exceptions.TIMEOUT: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet') + + dut.expect('Starting Advanced OTA example', timeout=30) + host_ip = get_host_ip4_by_dest_ip(ip_address) + + print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + bin_name)) + dut.write('https://' + host_ip + ':' + str(server_port) + '/' + bin_name) + dut.expect('Starting OTA...', timeout=60) + + # Restart device mid-download to trigger resumption + restart_device_with_random_delay(dut, 5, 15) + + # Validate that the device restarts correctly + dut.expect('Loaded app from partition at offset', timeout=180) + + try: + ip_address = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=60)[1].decode() + print(f'Connected to AP/Ethernet with IP: {ip_address}') + except pexpect.exceptions.TIMEOUT: + raise ValueError('ENV_TEST_FAILURE: Cannot connect to AP/Ethernet') + + dut.expect('Starting Advanced OTA example', timeout=30) + host_ip = get_host_ip4_by_dest_ip(ip_address) + + print('writing to device: {}'.format('https://' + host_ip + ':' + str(server_port) + '/' + bin_name)) + dut.write('https://' + host_ip + ':' + str(server_port) + '/' + bin_name) + dut.expect('Starting OTA...', timeout=60) + + # The server doesn't support Range requests, so it may return 416 or 200 OK. + # The OTA should detect this, close the connection, clear response buffer, + # and restart download from the beginning (works with OpenSSL s_server). + try: + dut.expect('restarting download from beginning', timeout=30) + print('Detected fallback to OTA without resumption (restart from beginning)') + except pexpect.exceptions.TIMEOUT: + # This is okay - the message might not be visible at current log level + pass + + # Verify that OTA completes successfully + dut.expect('upgrade successful. Rebooting ...', timeout=150) + + # After reboot, verify the device is running the new image + dut.expect('Loaded app from partition at offset', timeout=30) + dut.expect('OTA example app_main start', timeout=20) + + finally: + chunked_server.kill() + + @pytest.mark.flash_encryption_ota @pytest.mark.parametrize('config', ['ota_resumption_flash_enc'], indirect=True) @pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)