Skip to content

Commit 747223f

Browse files
vortigontmathieucarbou
authored andcommitted
fix: AsyncAbstractResponse might loose part of send buffer
AsyncAbstractResponse::_ack could allocate temp buffer with size larger than available sock buffer (i.e. to fit headers) and eventually lossing the remainder on transfer due to not checking if the complete data was added to sock buff. Refactoring code in favor of having a dedicated std::vector object acting as accumulating buffer and more carefull control on amount of data actually copied to sockbuff Closes #315 Added back MRE added overrides add AsyncWebServerRequest::clientRelease() method this will explicitly relese ownership of AsyncClient* object. Make it more clear on ownership change for SSE/WebSocket ci(pre-commit): Apply automatic fixes AsyncWebSocketResponse - keep request object till WS_EVT_CONNECT event is executed user code might use HTTP headers information from the request ci(pre-commit): Apply automatic fixes fix typo Add comment for slow response Cleanup wrong log line ci(pre-commit): Apply automatic fixes
1 parent c127402 commit 747223f

File tree

12 files changed

+449
-275
lines changed

12 files changed

+449
-275
lines changed

examples/LargeResponse/LargeResponse.ino

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,64 @@ private:
6565
size_t _sent = 0;
6666
};
6767

68+
// Code to reproduce issues:
69+
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
70+
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
71+
//
72+
// https://github.com/ESP32Async/ESPAsyncWebServer/pull/317#issuecomment-3421141039
73+
//
74+
// I cracked it.
75+
// So this is how it works:
76+
// That space that _tcp is writing to identified by CONFIG_TCP_SND_BUF_DEFAULT (and is value-matching with default TCP windows size which is very confusing itself).
77+
// The space returned by client()->write() and client->space() somehow might not be atomically/thread synced (had not dived that deep yet). So if first call to _fillBuffer is done via user-code thread and ended up with some small amount of data consumed and second one is done by _poll or _ack? returns full size again! This is where old code fails.
78+
// If you change your class this way it will fail 100%.
79+
class CustomResponseMRE : public AsyncAbstractResponse {
80+
public:
81+
explicit CustomResponseMRE() {
82+
_code = 200;
83+
_contentType = "text/plain";
84+
_sendContentLength = false;
85+
// add some useless headers
86+
addHeader("Clear-Site-Data", "Clears browsing data (e.g., cookies, storage, cache) associated with the requesting website.");
87+
addHeader(
88+
"No-Vary-Search", "Specifies a set of rules that define how a URL's query parameters will affect cache matching. These rules dictate whether the same "
89+
"URL with different URL parameters should be saved as separate browser cache entries"
90+
);
91+
}
92+
93+
bool _sourceValid() const override {
94+
return true;
95+
}
96+
97+
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
98+
if (fillChar == NULL) {
99+
fillChar = 'A';
100+
return RESPONSE_TRY_AGAIN;
101+
}
102+
if (_sent == RESPONSE_TRY_AGAIN) {
103+
Serial.println("Simulating temporary unavailability of data...");
104+
_sent = 0;
105+
return RESPONSE_TRY_AGAIN;
106+
}
107+
size_t remaining = totalResponseSize - _sent;
108+
if (remaining == 0) {
109+
return 0;
110+
}
111+
if (buflen > remaining) {
112+
buflen = remaining;
113+
}
114+
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
115+
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
116+
_sent += buflen;
117+
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
118+
return buflen;
119+
}
120+
121+
private:
122+
char fillChar = NULL;
123+
size_t _sent = 0;
124+
};
125+
68126
void setup() {
69127
Serial.begin(115200);
70128

@@ -77,14 +135,7 @@ void setup() {
77135
//
78136
// curl -v http://192.168.4.1/1 | grep -o '.' | sort | uniq -c
79137
//
80-
// Should output 16000 and the counts for each character from A to D
81-
//
82-
// Console:
83-
//
84-
// Filling 'A' @ index: 0, maxLen: 5652, toSend: 5652
85-
// Filling 'B' @ index: 5652, maxLen: 4308, toSend: 4308
86-
// Filling 'C' @ index: 9960, maxLen: 2888, toSend: 2888
87-
// Filling 'D' @ index: 12848, maxLen: 3152, toSend: 3152
138+
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
88139
//
89140
server.on("/1", HTTP_GET, [](AsyncWebServerRequest *request) {
90141
fillChar = 'A';
@@ -103,19 +154,22 @@ void setup() {
103154
//
104155
// curl -v http://192.168.4.1/2 | grep -o '.' | sort | uniq -c
105156
//
106-
// Should output 16000
107-
//
108-
// Console:
109-
//
110-
// Filling 'A' @ sent: 0, buflen: 5675
111-
// Filling 'B' @ sent: 5675, buflen: 4308
112-
// Filling 'C' @ sent: 9983, buflen: 5760
113-
// Filling 'D' @ sent: 15743, buflen: 257
157+
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
114158
//
115159
server.on("/2", HTTP_GET, [](AsyncWebServerRequest *request) {
116160
request->send(new CustomResponse());
117161
});
118162

163+
// Example to use a AsyncAbstractResponse
164+
//
165+
// curl -v http://192.168.4.1/3 | grep -o '.' | sort | uniq -c
166+
//
167+
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
168+
//
169+
server.on("/3", HTTP_GET, [](AsyncWebServerRequest *request) {
170+
request->send(new CustomResponseMRE());
171+
});
172+
119173
server.begin();
120174
}
121175

examples/PerfTests/PerfTests.ino

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ void setup() {
142142
//
143143
// time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output -
144144
//
145+
// THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG.
146+
// IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT
147+
//
148+
// CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC.
149+
//
145150
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) {
146151
requests = requests + 1;
147152
uint32_t d = request->getParam("d")->value().toInt();

examples/ServerSentEvents/ServerSentEvents.ino

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ void setup() {
7171
});
7272

7373
events.onConnect([](AsyncEventSourceClient *client) {
74-
Serial.printf("SSE Client connected! ID: %" PRIu32 "\n", client->lastId());
74+
Serial.printf("SSE Client connected!");
7575
client->send("hello!", NULL, millis(), 1000);
7676
});
7777

7878
events.onDisconnect([](AsyncEventSourceClient *client) {
79-
Serial.printf("SSE Client disconnected! ID: %" PRIu32 "\n", client->lastId());
79+
Serial.printf("SSE Client disconnected!");
8080
});
8181

8282
server.addHandler(&events);

examples/SlowChunkResponse/SlowChunkResponse.ino

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ void setup() {
114114
//
115115
// time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output -
116116
//
117+
// THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG.
118+
// IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT
119+
//
120+
// CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC.
121+
//
117122
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) {
118123
uint32_t d = request->getParam("d")->value().toInt();
119124
uint32_t l = request->getParam("l")->value().toInt();

src/AsyncEventSource.cpp

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ size_t AsyncEventSourceMessage::send(AsyncClient *client) {
143143

144144
// Client
145145

146-
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->client()), _server(server) {
146+
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->clientRelease()), _server(server) {
147147

148148
if (request->hasHeader(T_Last_Event_ID)) {
149149
_lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str());
@@ -181,9 +181,9 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A
181181
);
182182

183183
_server->_addClient(this);
184-
delete request;
185-
186184
_client->setNoDelay(true);
185+
// delete AsyncWebServerRequest object (and bound response) since we have the ownership on client connection now
186+
delete request;
187187
}
188188

189189
AsyncEventSourceClient::~AsyncEventSourceClient() {
@@ -470,8 +470,7 @@ void AsyncEventSource::_adjust_inflight_window() {
470470

471471
/* Response */
472472

473-
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) {
474-
_server = server;
473+
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) : _server(server) {
475474
_code = 200;
476475
_contentType = T_text_event_stream;
477476
_sendContentLength = false;
@@ -482,13 +481,24 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) {
482481
void AsyncEventSourceResponse::_respond(AsyncWebServerRequest *request) {
483482
String out;
484483
_assembleHead(out, request->version());
484+
// unbind client's onAck callback from AsyncWebServerRequest's, we will destroy it on next callback and steal the client,
485+
// can't do it now 'cause now we are in AsyncWebServerRequest::_onAck 's stack actually
486+
// here we are loosing time on one RTT delay, but with current design we can't get rid of Req/Resp objects other way
487+
_request = request;
488+
request->client()->onAck(
489+
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
490+
if (len) {
491+
static_cast<AsyncEventSourceResponse *>(r)->_switchClient();
492+
}
493+
},
494+
this
495+
);
485496
request->client()->write(out.c_str(), _headLength);
486497
_state = RESPONSE_WAIT_ACK;
487498
}
488499

489-
size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time __attribute__((unused))) {
490-
if (len) {
491-
new AsyncEventSourceClient(request, _server);
492-
}
493-
return 0;
494-
}
500+
void AsyncEventSourceResponse::_switchClient() {
501+
// AsyncEventSourceClient c-tor will take the ownership of AsyncTCP's client connection
502+
new AsyncEventSourceClient(_request, _server);
503+
// AsyncEventSourceClient c-tor would also delete _request and *this
504+
};

src/AsyncEventSource.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ class AsyncEventSourceClient {
141141
void _runQueue();
142142

143143
public:
144+
/**
145+
* @brief Construct a new Async Event Source Client object
146+
* @note constructor would take the ownership of of AsyncTCP's client pointer from `request` parameter and call delete on it!
147+
*
148+
* @param request
149+
* @param server
150+
*/
144151
AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server);
145152
~AsyncEventSourceClient();
146153

@@ -312,11 +319,16 @@ class AsyncEventSource : public AsyncWebHandler {
312319
class AsyncEventSourceResponse : public AsyncWebServerResponse {
313320
private:
314321
AsyncEventSource *_server;
322+
AsyncWebServerRequest *_request;
323+
// this call back will switch AsyncTCP client to SSE
324+
void _switchClient();
315325

316326
public:
317327
AsyncEventSourceResponse(AsyncEventSource *server);
318328
void _respond(AsyncWebServerRequest *request);
319-
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
329+
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override {
330+
return 0;
331+
};
320332
bool _sourceValid() const {
321333
return true;
322334
}

src/AsyncWebSocket.cpp

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,10 @@ size_t AsyncWebSocketMessage::send(AsyncClient *client) {
221221
const char *AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING";
222222
const size_t AWSC_PING_PAYLOAD_LEN = 22;
223223

224-
AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : _tempObject(NULL) {
225-
_client = request->client();
226-
_server = server;
227-
_clientId = _server->_getNextId();
228-
_status = WS_CONNECTED;
229-
_pstate = 0;
230-
_lastMessageTime = millis();
231-
_keepAlivePeriod = 0;
224+
AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server)
225+
: _client(client), _server(server), _clientId(_server->_getNextId()), _status(WS_CONNECTED), _pstate(0), _lastMessageTime(millis()), _keepAlivePeriod(0),
226+
_tempObject(NULL) {
227+
232228
_client->setRxTimeout(0);
233229
_client->onError(
234230
[](void *r, AsyncClient *c, int8_t error) {
@@ -272,7 +268,6 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
272268
},
273269
this
274270
);
275-
delete request;
276271
memset(&_pinfo, 0, sizeof(_pinfo));
277272
}
278273

@@ -806,7 +801,10 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient *client, AwsEventType typ
806801

807802
AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) {
808803
_clients.emplace_back(request, this);
804+
// we've just detached AsyncTCP client from AsyncWebServerRequest
809805
_handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0);
806+
// after user code completed CONNECT event callback we can delete req/response objects
807+
delete request;
810808
return &_clients.back();
811809
}
812810

@@ -1243,8 +1241,7 @@ AsyncWebSocketMessageBuffer *AsyncWebSocket::makeBuffer(const uint8_t *data, siz
12431241
* Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480
12441242
*/
12451243

1246-
AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) {
1247-
_server = server;
1244+
AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) : _server(server) {
12481245
_code = 101;
12491246
_sendContentLength = false;
12501247

@@ -1290,18 +1287,26 @@ void AsyncWebSocketResponse::_respond(AsyncWebServerRequest *request) {
12901287
request->client()->close();
12911288
return;
12921289
}
1290+
// unbind client's onAck callback from AsyncWebServerRequest's, we will destroy it on next callback and steal the client,
1291+
// can't do it now 'cause now we are in AsyncWebServerRequest::_onAck 's stack actually
1292+
// here we are loosing time on one RTT delay, but with current design we can't get rid of Req/Resp objects other way
1293+
_request = request;
1294+
request->client()->onAck(
1295+
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
1296+
if (len) {
1297+
static_cast<AsyncWebSocketResponse *>(r)->_switchClient();
1298+
}
1299+
},
1300+
this
1301+
);
12931302
String out;
12941303
_assembleHead(out, request->version());
12951304
request->client()->write(out.c_str(), _headLength);
12961305
_state = RESPONSE_WAIT_ACK;
12971306
}
12981307

1299-
size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
1300-
(void)time;
1301-
1302-
if (len) {
1303-
_server->_newClient(request);
1304-
}
1305-
1306-
return 0;
1308+
void AsyncWebSocketResponse::_switchClient() {
1309+
// detach client from request
1310+
_server->_newClient(_request);
1311+
// _newClient() would also destruct _request and *this
13071312
}

src/AsyncWebSocket.h

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,19 +211,18 @@ class AsyncWebSocketClient {
211211
AsyncWebSocket *_server;
212212
uint32_t _clientId;
213213
AwsClientStatus _status;
214+
uint8_t _pstate;
215+
uint32_t _lastMessageTime;
216+
uint32_t _keepAlivePeriod;
214217
#ifdef ESP32
215218
mutable std::recursive_mutex _lock;
216219
#endif
217220
std::deque<AsyncWebSocketControl> _controlQueue;
218221
std::deque<AsyncWebSocketMessage> _messageQueue;
219222
bool closeWhenFull = true;
220223

221-
uint8_t _pstate;
222224
AwsFrameInfo _pinfo;
223225

224-
uint32_t _lastMessageTime;
225-
uint32_t _keepAlivePeriod;
226-
227226
bool _queueControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false);
228227
bool _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false);
229228
void _runQueue();
@@ -232,7 +231,15 @@ class AsyncWebSocketClient {
232231
public:
233232
void *_tempObject;
234233

235-
AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server);
234+
AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server);
235+
236+
/**
237+
* @brief Construct a new Async Web Socket Client object
238+
* @note constructor would take the ownership of of AsyncTCP's client pointer from `request` parameter and call delete on it!
239+
* @param request
240+
* @param server
241+
*/
242+
AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : AsyncWebSocketClient(request->clientRelease(), server){};
236243
~AsyncWebSocketClient();
237244

238245
// client id increments for the given server
@@ -464,11 +471,16 @@ class AsyncWebSocketResponse : public AsyncWebServerResponse {
464471
private:
465472
String _content;
466473
AsyncWebSocket *_server;
474+
AsyncWebServerRequest *_request;
475+
// this call back will switch AsyncTCP client to WebSocket
476+
void _switchClient();
467477

468478
public:
469479
AsyncWebSocketResponse(const String &key, AsyncWebSocket *server);
470480
void _respond(AsyncWebServerRequest *request);
471-
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
481+
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override {
482+
return 0;
483+
};
472484
bool _sourceValid() const {
473485
return true;
474486
}

0 commit comments

Comments
 (0)