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
This commit is contained in:
nilesh.kale
2026-01-30 15:25:37 +05:30
parent bf3d4f0dd9
commit 4e434f8eb8
4 changed files with 125 additions and 12 deletions
@@ -1864,6 +1864,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;
@@ -732,6 +732,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.
+18 -11
View File
@@ -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);
}
@@ -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)