fix(esp_http_client): fix broken connection reuse

Closes https://github.com/espressif/esp-idf/issues/18430
This commit is contained in:
Ashish Sharma
2026-04-10 11:27:40 +08:00
parent 484e56869c
commit 9fa73b1d89
2 changed files with 334 additions and 8 deletions
@@ -0,0 +1,331 @@
# ESP HTTP Client State Machine
## States
| State | Value | Description |
|-------|-------|-------------|
| `HTTP_STATE_UNINIT` | 0 | Client not yet created |
| `HTTP_STATE_INIT` | 1 | Client created or connection closed (ready to connect) |
| `HTTP_STATE_CONNECTING` | 2 | Async connect in progress (async only) |
| `HTTP_STATE_CONNECTED` | 3 | TCP/TLS connection established |
| `HTTP_STATE_REQ_COMPLETE_HEADER` | 4 | Request headers sent |
| `HTTP_STATE_REQ_COMPLETE_DATA` | 5 | Request body (POST data) sent |
| `HTTP_STATE_RES_COMPLETE_HEADER` | 6 | Response headers received and parsed |
| `HTTP_STATE_RES_ON_DATA_START` | 7 | Response body ready to be read |
| `HTTP_STATE_RES_COMPLETE_DATA` | 8 | Defined but unused |
| `HTTP_STATE_CLOSE` | 9 | Defined but unused |
## Sync Mode
In sync (blocking) mode, `esp_http_client_perform()` drives the entire request/response
cycle in a single call. Each phase completes fully before moving to the next.
The `HTTP_STATE_CONNECTING` state is never observed — connect either succeeds or fails.
### State Transitions
```mermaid
stateDiagram-v2
[*] --> UNINIT
UNINIT --> INIT : esp_http_client_new()
INIT --> CONNECTED : esp_http_client_connect()\n[esp_transport_connect succeeds]
INIT --> ERROR : connect fails
CONNECTED --> REQ_COMPLETE_HEADER : esp_http_client_request_send()\n[headers written]
CONNECTED --> ERROR : request_send fails
REQ_COMPLETE_HEADER --> REQ_COMPLETE_DATA : esp_http_client_send_post_data()\n[body sent]
REQ_COMPLETE_HEADER --> ERROR : send_post_data fails
REQ_COMPLETE_DATA --> RES_COMPLETE_HEADER : http_on_headers_complete()\n[parser callback]
RES_COMPLETE_HEADER --> RES_ON_DATA_START : esp_http_client_fetch_headers()\n[headers parsed]
REQ_COMPLETE_DATA --> ERROR : fetch_headers fails
RES_ON_DATA_START --> CONNECTED : keep-alive\n[reuse connection]
RES_ON_DATA_START --> INIT : no keep-alive\n[esp_http_client_close()]
state ERROR <<choice>>
ERROR --> INIT : esp_http_client_close()
note right of CONNECTED
On keep-alive, state resets here.
Next perform() skips connection
and reuses existing transport.
end note
```
### `perform()` Flow
All switch-case stages fall through in a single invocation:
```mermaid
flowchart TD
A[perform called] --> B[esp_http_client_connect]
B -->|fail| ERR1[dispatch ERROR event\nreturn error]
B -->|ok| C[esp_http_client_request_send]
C -->|fail| ERR2[dispatch ERROR event\nreturn error]
C -->|ok| D[esp_http_client_send_post_data]
D -->|fail| ERR3[dispatch ERROR event\nreturn error]
D -->|ok| E[esp_http_client_fetch_headers]
E -->|fail| ERR4[dispatch ERROR event\nreturn error]
E -->|ok| F[esp_http_check_response]
F -->|redirect / auth| G[reset to CONNECTED\nre-enter loop]
F -->|ok| H[read response body]
H -->|keep-alive| I[reset to CONNECTED]
H -->|no keep-alive| J[esp_http_client_close → INIT]
I --> K{redirect?}
J --> K
K -->|yes| A
K -->|no| L[return ESP_OK]
```
## Async Mode
In async (non-blocking) mode, each phase can return `ESP_ERR_HTTP_EAGAIN`
when the underlying operation would block. The caller must retry `esp_http_client_perform()`,
which resumes from the saved state via the switch-case.
### State Transitions
```mermaid
stateDiagram-v2
[*] --> UNINIT
UNINIT --> INIT : esp_http_client_new()
INIT --> CONNECTING : connect_async returns ASYNC_TRANS_CONNECTING
INIT --> CONNECTED : connect_async succeeds immediately
CONNECTING --> CONNECTING : retry, connect_async still in progress
CONNECTING --> CONNECTED : retry, connect_async succeeds
CONNECTED --> REQ_COMPLETE_HEADER : esp_http_client_request_send()
REQ_COMPLETE_HEADER --> REQ_COMPLETE_DATA : esp_http_client_send_post_data()
REQ_COMPLETE_DATA --> RES_COMPLETE_HEADER : http_on_headers_complete() callback
RES_COMPLETE_HEADER --> RES_ON_DATA_START : esp_http_client_fetch_headers()
RES_ON_DATA_START --> CONNECTED : keep-alive\n[reuse connection]
RES_ON_DATA_START --> INIT : no keep-alive, esp_http_client_close()
```
### `perform()` Flow
Each stage can yield back to the caller. Dashed arrows show the yield-and-retry path:
```mermaid
flowchart TD
A[perform called] --> B{client->state?}
B -->|INIT / CONNECTING| C[esp_http_client_connect]
C -->|ASYNC_TRANS_CONNECTING| Y1[state = CONNECTING\nreturn EAGAIN to caller]
Y1 -.->|caller retries perform| A
C -->|fail| ERR1[return error]
C -->|ok, state = CONNECTED| D
B -->|CONNECTED| D[esp_http_client_request_send]
D -->|EAGAIN| Y2[state = CONNECTED\nreturn EAGAIN to caller]
Y2 -.->|caller retries perform| A
D -->|fail| ERR2[return error]
D -->|ok, state = REQ_COMPLETE_HEADER| E
B -->|REQ_COMPLETE_HEADER| E[esp_http_client_send_post_data]
E -->|EAGAIN| Y3[state = REQ_COMPLETE_HEADER\nreturn EAGAIN to caller]
Y3 -.->|caller retries perform| A
E -->|fail| ERR3[return error]
E -->|ok, state = REQ_COMPLETE_DATA| F
B -->|REQ_COMPLETE_DATA| F[esp_http_client_fetch_headers]
F -->|EAGAIN| Y4[state = REQ_COMPLETE_DATA\nreturn EAGAIN to caller]
Y4 -.->|caller retries perform| A
F -->|fail| ERR4[return error]
F -->|ok, state = RES_ON_DATA_START| G
B -->|RES_ON_DATA_START| G[esp_http_check_response]
G -->|redirect / auth| H[reset to CONNECTED\nre-enter loop]
G -->|ok| I[read response body]
I -->|keep-alive| J[reset to CONNECTED]
I -->|no keep-alive| K[close → INIT]
J --> L{redirect?}
K --> L
L -->|yes| A
L -->|no| M[return ESP_OK]
```
### Async Connection Retry Detail
Zooms into the CONNECTING retry loop, showing the interaction between the
caller, `perform()`, and `esp_http_client_connect()`:
```mermaid
sequenceDiagram
participant App as Application
participant P as perform()
participant C as esp_http_client_connect()
participant T as esp_transport_connect_async()
Note over App: state = INIT
App->>P: esp_http_client_perform()
P->>P: switch(state) hits INIT, falls through to CONNECTING
P->>C: esp_http_client_connect()
C->>C: state = CONNECTING
C->>C: select transport
C->>T: esp_transport_connect_async()
T-->>C: ASYNC_TRANS_CONNECTING
C-->>P: ESP_ERR_HTTP_CONNECTING
P-->>App: ESP_ERR_HTTP_EAGAIN
Note over App: state = CONNECTING, caller waits then retries
App->>P: esp_http_client_perform()
P->>P: switch(state) hits CONNECTING
P->>C: esp_http_client_connect()
C->>C: state = CONNECTING (no-op)
C->>T: esp_transport_connect_async()
T-->>C: ASYNC_TRANS_CONNECTING
C-->>P: ESP_ERR_HTTP_CONNECTING
P-->>App: ESP_ERR_HTTP_EAGAIN
Note over App: state = CONNECTING, caller retries again
App->>P: esp_http_client_perform()
P->>P: switch(state) hits CONNECTING
P->>C: esp_http_client_connect()
C->>T: esp_transport_connect_async()
T-->>C: ASYNC_TRANS_CONNECT_PASS (success)
C->>C: state = CONNECTED
C->>C: dispatch HTTP_EVENT_ON_CONNECTED
C-->>P: ESP_OK
P->>P: falls through to request_send...
```
## Connection Reuse (Keep-Alive)
When the server responds with `Connection: keep-alive`, the client reuses the
existing TCP/TLS connection for subsequent requests, avoiding the overhead of
a new handshake.
### Sync Connection Reuse
```mermaid
sequenceDiagram
participant App as Application
participant P as perform()
participant C as esp_http_client_connect()
Note over App: === First Request ===
App->>P: esp_http_client_perform()
P->>C: esp_http_client_connect()
C->>C: state (INIT) < CONNECTED → true
C->>C: select transport
C->>C: esp_transport_connect() succeeds
C->>C: state = CONNECTED
C-->>P: ESP_OK
P->>P: request_send → send_post_data → fetch_headers → read body
P->>P: http_should_keep_alive() → true
P->>P: state = CONNECTED (keep-alive reset)
P-->>App: ESP_OK
Note over App: state = CONNECTED, transport still open
Note over App: === Second Request (reuses connection) ===
App->>P: esp_http_client_perform()
P->>C: esp_http_client_connect()
C->>C: state (CONNECTED) < CONNECTED → false
Note over C: Skip transport selection and connect.<br/>Existing connection reused.
C-->>P: ESP_OK
P->>P: request_send → send_post_data → fetch_headers → read body
P-->>App: ESP_OK
```
### Async Connection Reuse
```mermaid
sequenceDiagram
participant App as Application
participant P as perform()
participant C as esp_http_client_connect()
participant T as esp_transport_connect_async()
Note over App: === First Request (multiple perform retries) ===
App->>P: esp_http_client_perform()
P->>C: esp_http_client_connect()
C->>C: state (INIT) < CONNECTED → true
C->>C: select transport
C->>T: esp_transport_connect_async()
T-->>C: ASYNC_TRANS_CONNECTING
C-->>P: ESP_ERR_HTTP_CONNECTING
P-->>App: ESP_ERR_HTTP_EAGAIN
App->>P: esp_http_client_perform() [retry]
P->>C: esp_http_client_connect()
C->>T: esp_transport_connect_async()
T-->>C: ASYNC_TRANS_CONNECT_PASS
C->>C: state = CONNECTED
C-->>P: ESP_OK
P->>P: request_send → send_post_data → fetch_headers → read body
P->>P: http_should_keep_alive() → true
P->>P: state = CONNECTED (keep-alive reset)
P-->>App: ESP_OK
Note over App: state = CONNECTED, transport still open
Note over App: === Second Request (reuses connection) ===
App->>P: esp_http_client_perform()
P->>C: esp_http_client_connect()
C->>C: state (CONNECTED) < CONNECTED → false
Note over C: Skip transport selection and connect.<br/>Existing connection reused.
C-->>P: ESP_OK
P->>P: request_send → send_post_data → fetch_headers → read body
P-->>App: ESP_OK
```
## Transition Details
### `esp_http_client_new()` → INIT
Sets state to `INIT` after allocating and initializing all client structures.
### `esp_http_client_connect()` → CONNECTING / CONNECTED
- **Sync**: Calls `esp_transport_connect()`. On success → `CONNECTED`. On failure → returns error.
- **Async**: Calls `esp_transport_connect_async()`.
- `ASYNC_TRANS_CONNECTING` → stays `CONNECTING`, returns `ESP_ERR_HTTP_CONNECTING`.
- Success → `CONNECTED`.
- Failure → returns error.
- Dispatches `HTTP_EVENT_ON_CONNECTED` on reaching `CONNECTED`.
- If already `CONNECTED` (keep-alive reuse), skips the entire connection block.
### `esp_http_client_request_send()` → REQ_COMPLETE_HEADER
Writes the request line and headers to the transport. Dispatches `HTTP_EVENT_HEADERS_SENT`.
### `esp_http_client_send_post_data()` → REQ_COMPLETE_DATA
Writes any remaining POST body data to the transport.
### `http_on_headers_complete()` → RES_COMPLETE_HEADER
HTTP parser callback invoked during `esp_http_client_fetch_headers()` when all
response headers are parsed. Dispatches `HTTP_EVENT_ON_HEADERS_COMPLETE`.
### `esp_http_client_fetch_headers()` → RES_ON_DATA_START
Set after the header-parsing loop completes. Client is now ready to read response body.
### Keep-alive reset → CONNECTED
In `esp_http_client_perform()`, after response is fully read, if
`http_should_keep_alive()` is true, state resets to `CONNECTED` for connection reuse.
The transport remains open — the next `perform()` call skips `esp_http_client_connect()`'s
connection block entirely.
### `esp_http_client_close()` → INIT
Closes the transport and resets state to `INIT`. Dispatches `HTTP_EVENT_DISCONNECTED`.
## Events
Events dispatched during the state machine lifecycle:
| Event | When | State After |
|-------|------|-------------|
| `HTTP_EVENT_ON_CONNECTED` | TCP/TLS connection established | `CONNECTED` |
| `HTTP_EVENT_HEADERS_SENT` | Request headers written | `REQ_COMPLETE_HEADER` |
| `HTTP_EVENT_ON_HEADERS_COMPLETE` | Response headers parsed | `RES_COMPLETE_HEADER` |
| `HTTP_EVENT_ON_DATA` | Response body chunk received | `RES_ON_DATA_START` |
| `HTTP_EVENT_ON_FINISH` | Response body fully received | `RES_ON_DATA_START` |
| `HTTP_EVENT_DISCONNECTED` | Connection closed | `INIT` |
| `HTTP_EVENT_ERROR` | Any error during perform | varies |
+3 -8
View File
@@ -1655,15 +1655,9 @@ static esp_err_t esp_http_client_connect(esp_http_client_handle_t client)
esp_http_client_close(client);
return err;
}
client->state = HTTP_STATE_CONNECTING;
if (client->state < HTTP_STATE_CONNECTED) {
#ifdef CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
// If the custom transport is enabled and defined, we skip the selection of appropriate transport from the list
// based on the scheme, since we already have the transport
if (!client->transport)
#endif
{
/* Select transport only if not already set (e.g., async retry or custom transport) */
if (!client->transport) {
ESP_LOGD(TAG, "Begin connect to: %s://%s:%d", client->connection_info.scheme, client->connection_info.host, client->connection_info.port);
client->transport = esp_transport_list_get_transport(client->transport_list, client->connection_info.scheme);
}
@@ -1688,6 +1682,7 @@ static esp_err_t esp_http_client_connect(esp_http_client_handle_t client)
return ESP_ERR_HTTP_CONNECT;
}
} else {
client->state = HTTP_STATE_CONNECTING;
int ret = esp_transport_connect_async(client->transport, client->connection_info.host, client->connection_info.port, client->timeout_ms);
if (ret == ASYNC_TRANS_CONNECT_FAIL) {
ESP_LOGE(TAG, "Connection failed");