mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-27 07:48:07 +03:00
Merge branch 'fix/esp_http_client_bodyless_204_304' into 'master'
fix(esp_http_client): do not wait for a body on a 204 or 304 response See merge request espressif/esp-idf!53059
This commit is contained in:
@@ -339,6 +339,24 @@ static int http_on_header_value(http_parser *parser, const char *at, size_t leng
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A response never carries a body on the wire when it answers a HEAD request,
|
||||
* or when its status is 204 No Content or 304 Not Modified. The server can
|
||||
* still send a Content-Length header in these cases: RFC 9110, section 8.6
|
||||
* allows it on a 304, where the value describes the selected representation.
|
||||
* The client must report such a response as complete, or it waits for bytes
|
||||
* that never arrive. */
|
||||
static bool http_status_is_bodyless(esp_http_client_handle_t client)
|
||||
{
|
||||
return client->response->status_code == HttpStatus_NoContent ||
|
||||
client->response->status_code == HttpStatus_NotModified;
|
||||
}
|
||||
|
||||
static bool http_response_is_bodyless(esp_http_client_handle_t client)
|
||||
{
|
||||
return client->connection_info.method == HTTP_METHOD_HEAD ||
|
||||
http_status_is_bodyless(client);
|
||||
}
|
||||
|
||||
static int http_on_headers_complete(http_parser *parser)
|
||||
{
|
||||
esp_http_client_handle_t client = parser->data;
|
||||
@@ -351,11 +369,11 @@ static int http_on_headers_complete(http_parser *parser)
|
||||
client->state = HTTP_STATE_RES_COMPLETE_HEADER;
|
||||
http_dispatch_event(client, HTTP_EVENT_ON_HEADERS_COMPLETE, NULL, 0);
|
||||
http_dispatch_event_to_event_loop(HTTP_EVENT_ON_HEADERS_COMPLETE, &client, sizeof(esp_http_client_handle_t));
|
||||
if (client->connection_info.method == HTTP_METHOD_HEAD) {
|
||||
if (http_response_is_bodyless(client)) {
|
||||
/* In a HTTP_RESPONSE parser returning '1' from on_headers_complete will tell the
|
||||
parser that it should not expect a body. This is used when receiving a response
|
||||
to a HEAD request which may contain 'Content-Length' or 'Transfer-Encoding: chunked'
|
||||
headers that indicate the presence of a body.*/
|
||||
that may contain 'Content-Length' or 'Transfer-Encoding: chunked' headers
|
||||
although no body follows.*/
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
@@ -1403,7 +1421,7 @@ static int esp_http_client_get_data(esp_http_client_handle_t client)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (client->connection_info.method == HTTP_METHOD_HEAD) {
|
||||
if (http_response_is_bodyless(client)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1430,6 +1448,13 @@ static int esp_http_client_get_data(esp_http_client_handle_t client)
|
||||
|
||||
bool esp_http_client_is_complete_data_received(esp_http_client_handle_t client)
|
||||
{
|
||||
/* No body follows the headers, so a declared Content-Length must not
|
||||
* count as outstanding data. esp_http_client_flush_response() loops on
|
||||
* this result, and esp_http_client_get_data() returns 0 here, so a false
|
||||
* result would never change. */
|
||||
if (http_response_is_bodyless(client)) {
|
||||
return true;
|
||||
}
|
||||
if (client->response->is_chunked) {
|
||||
if (!client->is_chunk_complete) {
|
||||
ESP_LOGD(TAG, "Chunks were not completely read");
|
||||
@@ -1465,7 +1490,9 @@ int esp_http_client_read(esp_http_client_handle_t client, char *buffer, int len)
|
||||
int need_read = len - ridx;
|
||||
bool is_data_remain = true;
|
||||
while (need_read > 0 && is_data_remain) {
|
||||
if (client->response->is_chunked) {
|
||||
if (http_response_is_bodyless(client)) {
|
||||
is_data_remain = false;
|
||||
} else if (client->response->is_chunked) {
|
||||
is_data_remain = !client->is_chunk_complete;
|
||||
} else {
|
||||
is_data_remain = client->response->data_process < client->response->content_length;
|
||||
@@ -1617,7 +1644,7 @@ esp_err_t esp_http_client_perform(esp_http_client_handle_t client)
|
||||
if (client->is_async && errno == EAGAIN) {
|
||||
return ESP_ERR_HTTP_EAGAIN;
|
||||
}
|
||||
if (client->connection_info.method != HTTP_METHOD_HEAD && !client->is_chunk_complete) {
|
||||
if (!http_response_is_bodyless(client) && !client->is_chunk_complete) {
|
||||
ESP_LOGE(TAG, "Incomplete chunked data received %d", ret);
|
||||
|
||||
// `ret` is the raw, untranslated value from esp_http_client_get_data()
|
||||
@@ -1645,7 +1672,7 @@ esp_err_t esp_http_client_perform(esp_http_client_handle_t client)
|
||||
if (client->is_async && errno == EAGAIN) {
|
||||
return ESP_ERR_HTTP_EAGAIN;
|
||||
}
|
||||
if (client->connection_info.method != HTTP_METHOD_HEAD && client->response->data_process < client->response->content_length) {
|
||||
if (!http_response_is_bodyless(client) && client->response->data_process < client->response->content_length) {
|
||||
ESP_LOGE(TAG, "Incomlete data received, ret=%d, %"PRId64"/%"PRId64" bytes", ret, client->response->data_process, client->response->content_length);
|
||||
|
||||
if (ret == ERR_TCP_TRANSPORT_CONNECTION_TIMEOUT) {
|
||||
@@ -1713,6 +1740,13 @@ int64_t esp_http_client_fetch_headers(esp_http_client_handle_t client)
|
||||
}
|
||||
client->state = HTTP_STATE_RES_ON_DATA_START;
|
||||
ESP_LOGD(TAG, "content_length = %"PRId64, client->response->content_length);
|
||||
/* A 204 or 304 has nothing to read, whatever its Content-Length says.
|
||||
* Return before the check below, which would mark it as chunked. A reply
|
||||
* to HEAD keeps the declared length for every status, 304 included:
|
||||
* callers use it as the resource size. */
|
||||
if (client->connection_info.method != HTTP_METHOD_HEAD && http_status_is_bodyless(client)) {
|
||||
return 0;
|
||||
}
|
||||
if (client->response->content_length <= 0) {
|
||||
client->response->is_chunked = true;
|
||||
return 0;
|
||||
|
||||
@@ -270,6 +270,7 @@ typedef struct {
|
||||
typedef enum {
|
||||
/* 2xx - Success */
|
||||
HttpStatus_Ok = 200,
|
||||
HttpStatus_NoContent = 204,
|
||||
HttpStatus_PartialContent = 206,
|
||||
|
||||
/* 3xx - Redirection */
|
||||
@@ -747,9 +748,14 @@ int esp_http_client_chunk_write_end(esp_http_client_handle_t client, bool last_c
|
||||
*
|
||||
* @return
|
||||
* - (0) if stream doesn't contain content-length header, or chunked encoding (checked by `esp_http_client_is_chunked_response`)
|
||||
* - (0) if the status is 204 No Content or 304 Not Modified and the request is not HEAD,
|
||||
* because no body follows. `esp_http_client_get_content_length()` still reports the
|
||||
* Content-Length header value.
|
||||
* - (-1: ESP_FAIL) if any errors
|
||||
* - (-ESP_ERR_HTTP_EAGAIN = -0x7007) if call is timed-out before any data was ready
|
||||
* - Download data length defined by content-length header
|
||||
* - Download data length defined by content-length header. For a response to a HEAD
|
||||
* request, with any status, this is the length that the server declares for the
|
||||
* resource. No body follows, and `esp_http_client_read()` returns 0.
|
||||
*/
|
||||
int64_t esp_http_client_fetch_headers(esp_http_client_handle_t client);
|
||||
|
||||
@@ -792,6 +798,12 @@ int esp_http_client_get_status_code(esp_http_client_handle_t client);
|
||||
* @brief Get http response content length (from header Content-Length)
|
||||
* the valid value if this function invoke after `esp_http_client_perform`
|
||||
*
|
||||
* @note A response to a HEAD request, a 204 No Content and a 304 Not Modified
|
||||
* carry no body, although the server can still send a Content-Length
|
||||
* header. This function reports the declared value, but
|
||||
* `esp_http_client_read()` returns 0 at once for such a response. Check
|
||||
* the status code and the request method before you read that many bytes.
|
||||
*
|
||||
* @param[in] client The esp_http_client handle
|
||||
*
|
||||
* @return
|
||||
@@ -927,6 +939,10 @@ esp_err_t esp_http_client_add_auth(esp_http_client_handle_t client);
|
||||
/**
|
||||
* @brief Checks if entire data in the response has been read without any error.
|
||||
*
|
||||
* @note A response to a HEAD request, a 204 No Content and a 304 Not Modified
|
||||
* carry no body, so this function returns true for them after the
|
||||
* headers are read, even if the server sent a Content-Length header.
|
||||
*
|
||||
* @param[in] client The esp_http_client handle
|
||||
*
|
||||
* @return
|
||||
|
||||
@@ -803,6 +803,100 @@ TEST_CASE("HTTP client survives multiple error/success cycles", "[esp_http_clien
|
||||
esp_http_client_cleanup(client);
|
||||
mock_http_transport_destroy(mock_transport);
|
||||
}
|
||||
/*
|
||||
* Bodyless responses. A 204 No Content and a 304 Not Modified never carry a
|
||||
* body on the wire, but a server can still send a Content-Length header.
|
||||
* RFC 9110, section 15.4.5 allows this on a 304, where the value describes the
|
||||
* selected representation. The client must not wait for that body.
|
||||
*/
|
||||
TEST_CASE("esp_http_client_perform() treats a 304 with Content-Length as bodyless", "[ESP HTTP CLIENT]")
|
||||
{
|
||||
/*
|
||||
* The mock serves headers only. A client that enters the body loop reads
|
||||
* again, gets 0 bytes, and reports ESP_ERR_HTTP_READ_TIMEOUT. A client that
|
||||
* treats the response as bodyless returns ESP_OK after a single read and
|
||||
* keeps the connection for the next request.
|
||||
*/
|
||||
static const char resp_304[] =
|
||||
"HTTP/1.1 304 Not Modified\r\nContent-Length: 10\r\nConnection: keep-alive\r\n\r\n";
|
||||
static const char resp_full[] =
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 4\r\nConnection: keep-alive\r\n\r\nABCD";
|
||||
|
||||
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
|
||||
mock_config.response_data = resp_304;
|
||||
mock_config.response_len = sizeof(resp_304) - 1;
|
||||
esp_transport_handle_t mock = mock_http_transport_create(&mock_config);
|
||||
TEST_ASSERT_NOT_NULL(mock);
|
||||
TEST_ASSERT_EQUAL(ESP_OK, mock_http_transport_queue_response(mock, resp_full, sizeof(resp_full) - 1));
|
||||
|
||||
esp_http_client_config_t config = { .url = "http://example.com/", .transport = mock };
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
TEST_ASSERT_NOT_NULL(client);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client));
|
||||
TEST_ASSERT_EQUAL(304, esp_http_client_get_status_code(client));
|
||||
/* The header value stays visible to the caller. */
|
||||
TEST_ASSERT_EQUAL(10, esp_http_client_get_content_length(client));
|
||||
|
||||
/* No body was expected, so the connection is kept and reused. */
|
||||
TEST_ASSERT_EQUAL(HTTP_STATE_CONNECTED, esp_http_client_get_state(client));
|
||||
|
||||
mock_http_transport_stats_t stats;
|
||||
TEST_ASSERT_EQUAL(ESP_OK, mock_http_transport_get_stats(mock, &stats));
|
||||
TEST_ASSERT_EQUAL(0, stats.close_calls);
|
||||
/* One read for the headers, and no second read for a body. */
|
||||
TEST_ASSERT_EQUAL(1, stats.read_calls);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_perform(client));
|
||||
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, mock_http_transport_get_stats(mock, &stats));
|
||||
TEST_ASSERT_EQUAL(1, stats.connect_calls);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_cleanup(client));
|
||||
mock_http_transport_destroy(mock);
|
||||
}
|
||||
|
||||
TEST_CASE("esp_http_client_read() returns 0 for a 204 with Content-Length", "[ESP HTTP CLIENT]")
|
||||
{
|
||||
/*
|
||||
* The streaming path must agree with perform(). A read on a 204 No Content
|
||||
* returns 0 at once instead of a wait on the socket.
|
||||
*/
|
||||
static const char resp_204[] =
|
||||
"HTTP/1.1 204 No Content\r\nContent-Length: 7\r\nConnection: keep-alive\r\n\r\n";
|
||||
|
||||
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
|
||||
mock_config.response_data = resp_204;
|
||||
mock_config.response_len = sizeof(resp_204) - 1;
|
||||
esp_transport_handle_t mock = mock_http_transport_create(&mock_config);
|
||||
TEST_ASSERT_NOT_NULL(mock);
|
||||
|
||||
esp_http_client_config_t config = { .url = "http://example.com/", .transport = mock };
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
TEST_ASSERT_NOT_NULL(client);
|
||||
|
||||
char buf[16];
|
||||
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_open(client, 0));
|
||||
/* No bytes to read, but the declared length stays available. */
|
||||
TEST_ASSERT_EQUAL(0, esp_http_client_fetch_headers(client));
|
||||
TEST_ASSERT_EQUAL(204, esp_http_client_get_status_code(client));
|
||||
TEST_ASSERT_EQUAL(7, esp_http_client_get_content_length(client));
|
||||
TEST_ASSERT_FALSE(esp_http_client_is_chunked_response(client));
|
||||
|
||||
TEST_ASSERT_EQUAL(0, esp_http_client_read(client, buf, sizeof(buf)));
|
||||
/* A second read must also return 0 and must not touch the socket. */
|
||||
mock_http_transport_stats_t stats;
|
||||
TEST_ASSERT_EQUAL(ESP_OK, mock_http_transport_get_stats(mock, &stats));
|
||||
int reads_after_first = stats.read_calls;
|
||||
TEST_ASSERT_EQUAL(0, esp_http_client_read(client, buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, mock_http_transport_get_stats(mock, &stats));
|
||||
TEST_ASSERT_EQUAL(reads_after_first, stats.read_calls);
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, esp_http_client_cleanup(client));
|
||||
mock_http_transport_destroy(mock);
|
||||
}
|
||||
|
||||
#endif // CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
|
||||
|
||||
void app_main(void)
|
||||
|
||||
@@ -653,7 +653,11 @@ static void http_perform_as_stream_reader(void)
|
||||
if (read_len <= 0) {
|
||||
ESP_LOGE(TAG, "Error read data");
|
||||
}
|
||||
buffer[read_len] = 0;
|
||||
/* A negative read_len is an error code, not a length: using it as an
|
||||
* index would write before the start of the buffer. */
|
||||
if (read_len >= 0) {
|
||||
buffer[read_len] = 0;
|
||||
}
|
||||
ESP_LOGD(TAG, "read_len = %d", read_len);
|
||||
}
|
||||
ESP_LOGI(TAG, "HTTP Stream reader Status = %d, content_length = %"PRId64,
|
||||
|
||||
Reference in New Issue
Block a user