Skip to content

Commit 652f70e

Browse files
authored
Merge pull request #316 from ESP32Async/wresp_315
fix: AsyncAbstractResponse might loose part of send buffer
2 parents c127402 + bb0dd46 commit 652f70e

File tree

12 files changed

+457
-275
lines changed

12 files changed

+457
-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)