Skip to content

Commit 46e984c

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
1 parent c127402 commit 46e984c

File tree

9 files changed

+437
-273
lines changed

9 files changed

+437
-273
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

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
}

src/ESPAsyncWebServer.h

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,19 @@ class AsyncWebServerRequest {
305305
AsyncClient *client() {
306306
return _client;
307307
}
308+
309+
/**
310+
* @brief release owned AsyncClient object
311+
* AsyncClient pointer will be abandoned in this instance,
312+
* the further ownership of the connection should be managed out of request's life-time scope
313+
* could be used for long lived connection like SSE or WebSockets
314+
* @note do not call this method unless you know what you are doing, otherwise it may lead to
315+
* memory leaks and connections lingering
316+
*
317+
* @return AsyncClient* pointer to released connection object
318+
*/
319+
AsyncClient *clientRelease();
320+
308321
uint8_t version() const {
309322
return _version;
310323
}
@@ -1336,8 +1349,10 @@ class AsyncWebServerResponse {
13361349
bool _sendContentLength;
13371350
bool _chunked;
13381351
size_t _headLength;
1352+
// amount of data sent for content part of the response (excluding all headers)
13391353
size_t _sentLength;
13401354
size_t _ackedLength;
1355+
// amount of response bytes (including all headers) written to sockbuff for delivery
13411356
size_t _writtenLength;
13421357
WebResponseState _state;
13431358

@@ -1394,7 +1409,20 @@ class AsyncWebServerResponse {
13941409
virtual bool _failed() const;
13951410
virtual bool _sourceValid() const;
13961411
virtual void _respond(AsyncWebServerRequest *request);
1397-
virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
1412+
1413+
/**
1414+
* @brief write next portion of response data to send buffs
1415+
* this method (re)fills tcp send buffers, it could be called either at will
1416+
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
1417+
*
1418+
* @param request - used to access client object
1419+
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
1420+
* @param time - time passed between last sent and received packet
1421+
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
1422+
*/
1423+
virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
1424+
return 0;
1425+
};
13981426
};
13991427

14001428
/*

0 commit comments

Comments
 (0)