Skip to content

Commit fd5b86d

Browse files
committed
Merge branch 'fix/http_server_async_requests_on_same_socket_blocks' into 'master'
Fix async requests on same socket blocking server Closes IDFGH-16057 and IDF-13859 See merge request espressif/esp-idf!41724
2 parents 63ad383 + dee9d76 commit fd5b86d

File tree

4 files changed

+114
-21
lines changed

4 files changed

+114
-21
lines changed

components/esp_http_server/src/esp_httpd_priv.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ extern "C" {
3737
/* Formats a log string to prepend context function name */
3838
#define LOG_FMT(x) "%s: " x, __func__
3939

40+
/**
41+
* @brief Control message data structure for internal use. Sent to control socket.
42+
*/
43+
struct httpd_ctrl_data {
44+
enum httpd_ctrl_msg {
45+
HTTPD_CTRL_SHUTDOWN,
46+
HTTPD_CTRL_WORK,
47+
HTTPD_CTRL_MAX,
48+
} hc_msg;
49+
httpd_work_fn_t hc_work;
50+
void *hc_work_arg;
51+
};
52+
4053
/**
4154
* @brief Thread related data for internal use
4255
*/

components/esp_http_server/src/httpd_main.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,6 @@ static esp_err_t httpd_accept_conn(struct httpd_data *hd, int listen_fd)
134134
return ESP_FAIL;
135135
}
136136

137-
struct httpd_ctrl_data {
138-
enum httpd_ctrl_msg {
139-
HTTPD_CTRL_SHUTDOWN,
140-
HTTPD_CTRL_WORK,
141-
} hc_msg;
142-
httpd_work_fn_t hc_work;
143-
void *hc_work_arg;
144-
};
145-
146137
esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *arg)
147138
{
148139
if (handle == NULL || work == NULL) {

components/esp_http_server/src/httpd_txrx.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <esp_http_server.h>
1313
#include "esp_httpd_priv.h"
1414
#include <netinet/tcp.h>
15+
#include "ctrl_sock.h"
1516

1617
static const char *TAG = "httpd_txrx";
1718

@@ -699,6 +700,11 @@ esp_err_t httpd_req_async_handler_complete(httpd_req_t *r)
699700
return ESP_ERR_INVALID_ARG;
700701
}
701702

703+
// Get server handle and control socket info before freeing the request
704+
struct httpd_data *hd = (struct httpd_data *) r->handle;
705+
int msg_fd = hd->msg_fd;
706+
int port = hd->config.ctrl_port;
707+
702708
struct httpd_req_aux *ra = r->aux;
703709
ra->sd->for_async_req = false;
704710
free(ra->scratch);
@@ -709,6 +715,18 @@ esp_err_t httpd_req_async_handler_complete(httpd_req_t *r)
709715
free(r->aux);
710716
free(r);
711717

718+
// Send a dummy control message(httpd_ctrl_data) to unblock the main HTTP server task from the select() call.
719+
// Since the current connection FD was marked as inactive for async requests, the main task
720+
// will now re-add this FD to its select() descriptor list. This ensures that subsequent requests
721+
// on the same FD are processed correctly
722+
struct httpd_ctrl_data msg = {.hc_msg = HTTPD_CTRL_MAX};
723+
int ret = cs_send_to_ctrl_sock(msg_fd, port, &msg, sizeof(msg));
724+
if (ret < 0) {
725+
ESP_LOGW(TAG, LOG_FMT("failed to send socket notification"));
726+
return ESP_FAIL;
727+
}
728+
729+
ESP_LOGD(TAG, LOG_FMT("socket notification sent"));
712730
return ESP_OK;
713731
}
714732

examples/protocols/http_server/async_handlers/pytest_http_server_async.py

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ def test_http_server_async_handler_multiple_long_requests(dut: Dut) -> None:
1616
# Get binary file
1717
binary_file = os.path.join(dut.app.binary_path, 'simple.bin')
1818
bin_size = os.path.getsize(binary_file)
19-
logging.info('http_server_bin_size : {}KB'.format(bin_size // 1024))
19+
logging.info(f'http_server_bin_size : {bin_size // 1024}KB')
2020
logging.info('Waiting to connect with Ethernet')
2121

2222
# Parse IP address of Ethernet
2323
got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode()
2424
got_port = 80 # Assuming the server is running on port 80
25-
logging.info('Got IP : {}'.format(got_ip))
26-
logging.info('Connecting to server at {}:{}'.format(got_ip, got_port))
25+
logging.info(f'Got IP : {got_ip}')
26+
logging.info(f'Connecting to server at {got_ip}:{got_port}')
2727

2828
# Create two HTTP connections for long requests
2929
conn_long1 = http.client.HTTPConnection(got_ip, got_port, timeout=30)
@@ -51,8 +51,8 @@ def test_http_server_async_handler_multiple_long_requests(dut: Dut) -> None:
5151
response_long1 = conn_long1.getresponse()
5252
response_long2 = conn_long2.getresponse()
5353

54-
logging.info('Response status for first long URI: {}'.format(response_long1.status))
55-
logging.info('Response status for second long URI: {}'.format(response_long2.status))
54+
logging.info(f'Response status for first long URI: {response_long1.status}')
55+
logging.info(f'Response status for second long URI: {response_long2.status}')
5656

5757
assert response_long1.status == 200, 'Failed to access first long URI'
5858
assert response_long2.status == 200, 'Failed to access second long URI'
@@ -67,38 +67,109 @@ def test_http_server_async_handler(dut: Dut) -> None:
6767
# Get binary file
6868
binary_file = os.path.join(dut.app.binary_path, 'simple.bin')
6969
bin_size = os.path.getsize(binary_file)
70-
logging.info('http_server_bin_size : {}KB'.format(bin_size // 1024))
70+
logging.info(f'http_server_bin_size : {bin_size // 1024}KB')
7171
logging.info('Waiting to connect with Ethernet')
7272

7373
# Parse IP address of Ethernet
7474
got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode()
7575
got_port = 80 # Assuming the server is running on port 80
76-
logging.info('Got IP : {}'.format(got_ip))
77-
logging.info('Connecting to server at {}:{}'.format(got_ip, got_port))
76+
logging.info(f'Got IP : {got_ip}')
77+
logging.info(f'Connecting to server at {got_ip}:{got_port}')
7878

7979
# Create HTTP connection
8080
conn_long = http.client.HTTPConnection(got_ip, got_port, timeout=15)
8181

8282
# Test long URI
8383
long_uri = '/long'
84-
logging.info('Sending request to long URI: {}'.format(long_uri))
84+
logging.info(f'Sending request to long URI: {long_uri}')
8585
conn_long.request('GET', long_uri)
8686
dut.expect('uri: /long', timeout=30)
8787
response_long = conn_long.getresponse()
88-
logging.info('Response status for long URI: {}'.format(response_long.status))
88+
logging.info(f'Response status for long URI: {response_long.status}')
8989
assert response_long.status == 200, 'Failed to access long URI'
9090

9191
# Test quick URI
9292
for i in range(3):
9393
conn_quick = http.client.HTTPConnection(got_ip, got_port, timeout=15)
9494
quick_uri = '/quick'
95-
logging.info('Sending request to quick URI: {}'.format(quick_uri))
95+
logging.info(f'Sending request to quick URI: {quick_uri}')
9696
conn_quick.request('GET', quick_uri)
9797
time.sleep(1) # Adding a delay of 1 second before getting the response
9898
response_quick = conn_quick.getresponse()
9999
dut.expect('uri: /quick', timeout=30)
100-
logging.info('Response status for quick URI: {}'.format(response_quick.status))
100+
logging.info(f'Response status for quick URI: {response_quick.status}')
101101
assert response_quick.status == 200, 'Failed to access quick URI'
102102
conn_quick.close()
103103

104104
conn_long.close()
105+
106+
107+
@pytest.mark.ethernet
108+
@idf_parametrize('target', ['esp32'], indirect=['target'])
109+
def test_http_server_async_handler_same_session_sequential(dut: Dut) -> None:
110+
"""
111+
Test that verifies async completion fix:
112+
1. Send /long request (async, 60 seconds)
113+
2. Wait for completion
114+
3. Send another request on same session
115+
4. Verify second request works (doesn't get stuck)
116+
"""
117+
# Get binary file
118+
binary_file = os.path.join(dut.app.binary_path, 'simple.bin')
119+
bin_size = os.path.getsize(binary_file)
120+
logging.info(f'http_server_bin_size : {bin_size // 1024}KB')
121+
logging.info('Waiting to connect with Ethernet')
122+
123+
# Parse IP address of Ethernet
124+
got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode()
125+
got_port = 80 # Assuming the server is running on port 80
126+
logging.info(f'Got IP : {got_ip}')
127+
logging.info(f'Connecting to server at {got_ip}:{got_port}')
128+
129+
# Create HTTP connection for same session testing
130+
conn = http.client.HTTPConnection(got_ip, got_port, timeout=70) # Longer timeout for async
131+
132+
# Test 1: Send /long request (async, 60 seconds)
133+
logging.info('=== Test 1: Sending /long request (async) ===')
134+
conn.request('GET', '/long?test=sequential1')
135+
136+
# Verify request is received and processed
137+
dut.expect('uri: /long', timeout=30)
138+
dut.expect('Found query string => test=sequential1', timeout=30)
139+
140+
# Wait for async completion (60 seconds + buffer)
141+
logging.info('Waiting for async /long request to complete (60 seconds)...')
142+
start_time = time.time()
143+
144+
# Get response (this will block until async handler completes)
145+
response_long = conn.getresponse()
146+
completion_time = time.time() - start_time
147+
148+
logging.info(f'Response status for /long: {response_long.status}')
149+
logging.info(f'Async request completed in {completion_time:.2f} seconds')
150+
assert response_long.status == 200, 'Failed to access /long URI'
151+
152+
# Verify we got the full response (should contain 60 ticks)
153+
response_data = response_long.read().decode()
154+
assert 'req: 1' in response_data, 'Expected request count in response'
155+
assert '59' in response_data, 'Expected final tick (59) in response'
156+
157+
# Test 3: Send another /long request on same session
158+
logging.info('=== Test 2: Sending another /long request on same session ===')
159+
conn.request('GET', '/long?test=sequential3')
160+
161+
# Verify third request is processed
162+
dut.expect('uri: /long', timeout=30)
163+
dut.expect('Found query string => test=sequential3', timeout=30)
164+
165+
# Get response for third request
166+
response_long2 = conn.getresponse()
167+
logging.info(f'Response status for second /long: {response_long2.status}')
168+
assert response_long2.status == 200, 'Failed to access second /long URI on same session'
169+
170+
# Verify we got the full response
171+
response_data2 = response_long2.read().decode()
172+
assert 'req: 2' in response_data2, 'Expected request count 2 in response'
173+
174+
conn.close()
175+
logging.info('=== Test completed successfully: Same session sequential requests work ===')

0 commit comments

Comments
 (0)