Skip to content

Commit c8026aa

Browse files
Merge pull request #41 from ESP32Async/feat/continuation
feat(http) Request Continuation support
2 parents 29de033 + f891381 commit c8026aa

File tree

5 files changed

+304
-3
lines changed

5 files changed

+304
-3
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
3+
4+
//
5+
// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later.
6+
//
7+
8+
#include <Arduino.h>
9+
#ifdef ESP32
10+
#include <AsyncTCP.h>
11+
#include <WiFi.h>
12+
#elif defined(ESP8266)
13+
#include <ESP8266WiFi.h>
14+
#include <ESPAsyncTCP.h>
15+
#elif defined(TARGET_RP2040)
16+
#include <WebServer.h>
17+
#include <WiFi.h>
18+
#endif
19+
20+
#include <ESPAsyncWebServer.h>
21+
#include <list>
22+
#include <mutex>
23+
24+
static AsyncWebServer server(80);
25+
26+
// request handler that is saved from the paused request to communicate with Serial
27+
static String message;
28+
static AsyncWebServerRequestPtr serialRequest;
29+
30+
// request handler that is saved from the paused request to communicate with GPIO
31+
static uint8_t pin = 35;
32+
static AsyncWebServerRequestPtr gpioRequest;
33+
34+
void setup() {
35+
Serial.begin(115200);
36+
37+
#ifndef CONFIG_IDF_TARGET_ESP32H2
38+
WiFi.mode(WIFI_AP);
39+
WiFi.softAP("esp-captive");
40+
#endif
41+
42+
// Post a message that will be sent to the Serial console, and pause the request until the user types a key
43+
//
44+
// curl -v -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "question=Name%3F%20" http://192.168.4.1/serial
45+
//
46+
// curl output should show "Answer: [y/n]" as the response
47+
server.on("/serial", HTTP_POST, [](AsyncWebServerRequest *request) {
48+
message = request->getParam("question", true)->value();
49+
serialRequest = request->pause();
50+
});
51+
52+
// Wait for a GPIO to be high
53+
//
54+
// curl -v http://192.168.4.1/gpio
55+
//
56+
// curl output should show "GPIO is high!" as the response
57+
server.on("/gpio", HTTP_GET, [](AsyncWebServerRequest *request) {
58+
gpioRequest = request->pause();
59+
});
60+
61+
pinMode(pin, INPUT);
62+
63+
server.begin();
64+
}
65+
66+
void loop() {
67+
delay(500);
68+
69+
// Check for a high voltage on the RX1 pin
70+
if (digitalRead(pin) == HIGH) {
71+
if (auto request = gpioRequest.lock()) {
72+
request->send(200, "text/plain", "GPIO is high!");
73+
}
74+
}
75+
76+
// check for an incoming message from the Serial console
77+
if (message.length()) {
78+
Serial.printf("%s", message.c_str());
79+
// drops buffer
80+
while (Serial.available()) {
81+
Serial.read();
82+
}
83+
Serial.setTimeout(10000);
84+
String response = Serial.readStringUntil('\n'); // waits for a key to be pressed
85+
Serial.println();
86+
message = emptyString;
87+
if (auto request = serialRequest.lock()) {
88+
request->send(200, "text/plain", "Answer: " + response);
89+
}
90+
}
91+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
3+
4+
//
5+
// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later.
6+
//
7+
8+
#include <Arduino.h>
9+
#ifdef ESP32
10+
#include <AsyncTCP.h>
11+
#include <WiFi.h>
12+
#elif defined(ESP8266)
13+
#include <ESP8266WiFi.h>
14+
#include <ESPAsyncTCP.h>
15+
#elif defined(TARGET_RP2040)
16+
#include <WebServer.h>
17+
#include <WiFi.h>
18+
#endif
19+
20+
#include <ESPAsyncWebServer.h>
21+
22+
#include <list>
23+
#include <memory>
24+
#include <mutex>
25+
26+
static AsyncWebServer server(80);
27+
28+
// ===============================================================
29+
// The code below is used to simulate some long running operations
30+
// ===============================================================
31+
32+
typedef struct {
33+
size_t id;
34+
AsyncWebServerRequestPtr requestPtr;
35+
uint8_t data;
36+
} LongRunningOperation;
37+
38+
static std::list<std::unique_ptr<LongRunningOperation>> longRunningOperations;
39+
static size_t longRunningOperationsCount = 0;
40+
#ifdef ESP32
41+
static std::mutex longRunningOperationsMutex;
42+
#endif
43+
44+
static void startLongRunningOperation(AsyncWebServerRequestPtr &&requestPtr) {
45+
#ifdef ESP32
46+
std::lock_guard<std::mutex> lock(longRunningOperationsMutex);
47+
#endif
48+
49+
// LongRunningOperation *op = new LongRunningOperation();
50+
std::unique_ptr<LongRunningOperation> op(new LongRunningOperation());
51+
op->id = ++longRunningOperationsCount;
52+
op->data = 10;
53+
54+
// you need to hold the AsyncWebServerRequestPtr returned by pause();
55+
// This object is authorized to leave the scope of the request handler.
56+
op->requestPtr = std::move(requestPtr);
57+
58+
Serial.printf("[%u] Start long running operation for %" PRIu8 " seconds...\n", op->id, op->data);
59+
longRunningOperations.push_back(std::move(op));
60+
}
61+
62+
static bool processLongRunningOperation(LongRunningOperation *op) {
63+
// request was deleted ?
64+
if (op->requestPtr.expired()) {
65+
Serial.printf("[%u] Request was deleted - stopping long running operation\n", op->id);
66+
return true; // operation finished
67+
}
68+
69+
// processing the operation
70+
Serial.printf("[%u] Long running operation processing... %" PRIu8 " seconds left\n", op->id, op->data);
71+
72+
// check if we have finished ?
73+
op->data--;
74+
if (op->data) {
75+
// not finished yet
76+
return false;
77+
}
78+
79+
// Try to get access to the request pointer if it is still exist.
80+
// If there has been a disconnection during that time, the pointer won't be valid anymore
81+
if (auto request = op->requestPtr.lock()) {
82+
Serial.printf("[%u] Long running operation finished! Sending back response...\n", op->id);
83+
request->send(200, "text/plain", String(op->id) + " ");
84+
85+
} else {
86+
Serial.printf("[%u] Long running operation finished, but request was deleted!\n", op->id);
87+
}
88+
89+
return true; // operation finished
90+
}
91+
92+
/// ==========================================================
93+
94+
void setup() {
95+
Serial.begin(115200);
96+
97+
#ifndef CONFIG_IDF_TARGET_ESP32H2
98+
WiFi.mode(WIFI_AP);
99+
WiFi.softAP("esp-captive");
100+
#endif
101+
102+
// Add a middleware to see how pausing a request affects the middleware chain
103+
server.addMiddleware([](AsyncWebServerRequest *request, ArMiddlewareNext next) {
104+
Serial.printf("Middleware chain start\n");
105+
106+
// continue to the next middleware, and at the end the request handler
107+
next();
108+
109+
// we can check the request pause state after the handler was executed
110+
if (request->isPaused()) {
111+
Serial.printf("Request was paused!\n");
112+
}
113+
114+
Serial.printf("Middleware chain ends\n");
115+
});
116+
117+
// HOW TO RUN THIS EXAMPLE:
118+
//
119+
// 1. Open several terminals to trigger some requests concurrently that will be paused with:
120+
// > time curl -v http://192.168.4.1/
121+
//
122+
// 2. Look at the output of the Serial console to see how the middleware chain is executed
123+
// and to see the long running operations being processed and resume the requests.
124+
//
125+
// 3. You can try close your curl command to cancel the request and check that the request is deleted.
126+
// Note: in case the network is disconnected, the request will be deleted.
127+
//
128+
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
129+
// Print a message in case the request is disconnected (network disconnection, client close, etc.)
130+
request->onDisconnect([]() {
131+
Serial.printf("Request was disconnected!\n");
132+
});
133+
134+
// Instruct ESPAsyncWebServer to pause the request and get a AsyncWebServerRequestPtr to be able to access the request later.
135+
// The AsyncWebServerRequestPtr is the ONLY object authorized to leave the scope of the request handler.
136+
// The Middleware chain will continue to run until the end after this handler exit, but the request will be paused and will not
137+
// be sent to the client until send() is called later.
138+
Serial.printf("Pausing request...\n");
139+
AsyncWebServerRequestPtr requestPtr = request->pause();
140+
141+
// start our long operation...
142+
startLongRunningOperation(std::move(requestPtr));
143+
});
144+
145+
server.begin();
146+
}
147+
148+
static uint32_t lastTime = 0;
149+
150+
void loop() {
151+
if (millis() - lastTime >= 1000) {
152+
153+
#ifdef ESP32
154+
Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
155+
std::lock_guard<std::mutex> lock(longRunningOperationsMutex);
156+
#endif
157+
158+
// process all long running operations
159+
longRunningOperations.remove_if([](const std::unique_ptr<LongRunningOperation> &op) {
160+
return processLongRunningOperation(op.get());
161+
});
162+
163+
lastTime = millis();
164+
}
165+
}

platformio.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ lib_dir = .
1919
src_dir = examples/PerfTests
2020
; src_dir = examples/RateLimit
2121
; src_dir = examples/Redirect
22+
; src_dir = examples/RequestContinuation
23+
; src_dir = examples/RequestContinuationComplete
2224
; src_dir = examples/ResumableDownload
2325
; src_dir = examples/Rewrite
2426
; src_dir = examples/ServerSentEvents

src/ESPAsyncWebServer.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ typedef enum {
179179
typedef std::function<size_t(uint8_t *, size_t, size_t)> AwsResponseFiller;
180180
typedef std::function<String(const String &)> AwsTemplateProcessor;
181181

182+
using AsyncWebServerRequestPtr = std::weak_ptr<AsyncWebServerRequest>;
183+
182184
class AsyncWebServerRequest {
183185
using File = fs::File;
184186
using FS = fs::FS;
@@ -192,8 +194,9 @@ class AsyncWebServerRequest {
192194
AsyncWebServerResponse *_response;
193195
ArDisconnectHandler _onDisconnectfn;
194196

195-
// response is sent
196-
bool _sent = false;
197+
bool _sent = false; // response is sent
198+
bool _paused = false; // request is paused (request continuation)
199+
std::shared_ptr<AsyncWebServerRequest> _this; // shared pointer to this request
197200

198201
String _temp;
199202
uint8_t _parseState;
@@ -484,6 +487,21 @@ class AsyncWebServerRequest {
484487
#endif
485488
AsyncWebServerResponse *beginResponse_P(int code, const String &contentType, PGM_P content, AwsTemplateProcessor callback = nullptr);
486489

490+
/**
491+
* @brief Request Continuation: this function pauses the current request and returns a weak pointer (AsyncWebServerRequestPtr is a std::weak_ptr) to the request in order to reuse it later on.
492+
* The middelware chain will continue to be processed until the end, but no response will be sent.
493+
* To resume operations (send the request), the request must be retrieved from the weak pointer and a send() function must be called.
494+
* AsyncWebServerRequestPtr is the only object allowed to exist the scope of the request handler.
495+
* @warning This function should be called from within the context of a request (in a handler or middleware for example).
496+
* @warning While the request is paused, if the client aborts the request, the latter will be disconnected and deleted.
497+
* So it is the responsibility of the user to check the validity of the request pointer (AsyncWebServerRequestPtr) before using it by calling lock() and/or expired().
498+
*/
499+
AsyncWebServerRequestPtr pause();
500+
501+
bool isPaused() const {
502+
return _paused;
503+
}
504+
487505
/**
488506
* @brief Get the Request parameter by name
489507
*

src/WebRequest.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '='))
1111

12+
static void doNotDelete(AsyncWebServerRequest *) {}
13+
1214
using namespace asyncsrv;
1315

1416
enum {
@@ -75,6 +77,8 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c)
7577
}
7678

7779
AsyncWebServerRequest::~AsyncWebServerRequest() {
80+
_this.reset();
81+
7882
_headers.clear();
7983

8084
_pathParams.clear();
@@ -691,7 +695,7 @@ void AsyncWebServerRequest::_runMiddlewareChain() {
691695
}
692696

693697
void AsyncWebServerRequest::_send() {
694-
if (!_sent) {
698+
if (!_sent && !_paused) {
695699
// log_d("AsyncWebServerRequest::_send()");
696700

697701
// user did not create a response ?
@@ -711,6 +715,18 @@ void AsyncWebServerRequest::_send() {
711715
}
712716
}
713717

718+
AsyncWebServerRequestPtr AsyncWebServerRequest::pause() {
719+
if (_paused) {
720+
return _this;
721+
}
722+
client()->setRxTimeout(0);
723+
// this shared ptr will hold the request pointer until it gets destroyed following a disconnect.
724+
// this is just used as a holder providing weak observers, so the deleter is a no-op.
725+
_this = std::shared_ptr<AsyncWebServerRequest>(this, doNotDelete);
726+
_paused = true;
727+
return _this;
728+
}
729+
714730
size_t AsyncWebServerRequest::headers() const {
715731
return _headers.size();
716732
}
@@ -887,13 +903,22 @@ AsyncWebServerResponse *AsyncWebServerRequest::beginResponse_P(int code, const S
887903
}
888904

889905
void AsyncWebServerRequest::send(AsyncWebServerResponse *response) {
906+
// request is already sent on the wire ?
890907
if (_sent) {
891908
return;
892909
}
910+
911+
// if we already had a response, delete it and replace it with the new one
893912
if (_response) {
894913
delete _response;
895914
}
896915
_response = response;
916+
917+
// if request was paused, we need to send the response now
918+
if (_paused) {
919+
_paused = false;
920+
_send();
921+
}
897922
}
898923

899924
void AsyncWebServerRequest::redirect(const char *url, int code) {

0 commit comments

Comments
 (0)