mirror of
https://github.com/espressif/esp-idf.git
synced 2026-04-27 19:13:21 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user