From f891381a8ecc37e5cd08f29cd89e20eb15cb0a14 Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Thu, 30 Jan 2025 23:04:44 +0100 Subject: [PATCH] Request Continuation support **Request Continuation** is the ability to pause the processing of a request (the actual sending over the network) to be able to let another task commit the response on the network later. This is a common supported use case amongst web servers. A uage example is described in the following discussion: https://github.com/ESP32Async/ESPAsyncWebServer/discussions/34 Currently, the only supported way is to use chunk encoding and return `RESPONSE_TRY_AGAIN` from the callback until the processing is done somewhere else, then send the response in the chunk buffer, when the processing is completed. --- .../RequestContinuation.ino | 91 ++++++++++ .../RequestContinuationComplete.ino | 165 ++++++++++++++++++ platformio.ini | 2 + src/ESPAsyncWebServer.h | 22 ++- src/WebRequest.cpp | 27 ++- 5 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 examples/RequestContinuation/RequestContinuation.ino create mode 100644 examples/RequestContinuationComplete/RequestContinuationComplete.ino diff --git a/examples/RequestContinuation/RequestContinuation.ino b/examples/RequestContinuation/RequestContinuation.ino new file mode 100644 index 000000000..c05250490 --- /dev/null +++ b/examples/RequestContinuation/RequestContinuation.ino @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later. +// + +#include +#ifdef ESP32 +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) +#include +#include +#endif + +#include +#include +#include + +static AsyncWebServer server(80); + +// request handler that is saved from the paused request to communicate with Serial +static String message; +static AsyncWebServerRequestPtr serialRequest; + +// request handler that is saved from the paused request to communicate with GPIO +static uint8_t pin = 35; +static AsyncWebServerRequestPtr gpioRequest; + +void setup() { + Serial.begin(115200); + +#ifndef CONFIG_IDF_TARGET_ESP32H2 + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // Post a message that will be sent to the Serial console, and pause the request until the user types a key + // + // curl -v -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "question=Name%3F%20" http://192.168.4.1/serial + // + // curl output should show "Answer: [y/n]" as the response + server.on("/serial", HTTP_POST, [](AsyncWebServerRequest *request) { + message = request->getParam("question", true)->value(); + serialRequest = request->pause(); + }); + + // Wait for a GPIO to be high + // + // curl -v http://192.168.4.1/gpio + // + // curl output should show "GPIO is high!" as the response + server.on("/gpio", HTTP_GET, [](AsyncWebServerRequest *request) { + gpioRequest = request->pause(); + }); + + pinMode(pin, INPUT); + + server.begin(); +} + +void loop() { + delay(500); + + // Check for a high voltage on the RX1 pin + if (digitalRead(pin) == HIGH) { + if (auto request = gpioRequest.lock()) { + request->send(200, "text/plain", "GPIO is high!"); + } + } + + // check for an incoming message from the Serial console + if (message.length()) { + Serial.printf("%s", message.c_str()); + // drops buffer + while (Serial.available()) { + Serial.read(); + } + Serial.setTimeout(10000); + String response = Serial.readStringUntil('\n'); // waits for a key to be pressed + Serial.println(); + message = emptyString; + if (auto request = serialRequest.lock()) { + request->send(200, "text/plain", "Answer: " + response); + } + } +} diff --git a/examples/RequestContinuationComplete/RequestContinuationComplete.ino b/examples/RequestContinuationComplete/RequestContinuationComplete.ino new file mode 100644 index 000000000..06f359a7d --- /dev/null +++ b/examples/RequestContinuationComplete/RequestContinuationComplete.ino @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later. +// + +#include +#ifdef ESP32 +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) +#include +#include +#endif + +#include + +#include +#include +#include + +static AsyncWebServer server(80); + +// =============================================================== +// The code below is used to simulate some long running operations +// =============================================================== + +typedef struct { + size_t id; + AsyncWebServerRequestPtr requestPtr; + uint8_t data; +} LongRunningOperation; + +static std::list> longRunningOperations; +static size_t longRunningOperationsCount = 0; +#ifdef ESP32 +static std::mutex longRunningOperationsMutex; +#endif + +static void startLongRunningOperation(AsyncWebServerRequestPtr &&requestPtr) { +#ifdef ESP32 + std::lock_guard lock(longRunningOperationsMutex); +#endif + + // LongRunningOperation *op = new LongRunningOperation(); + std::unique_ptr op(new LongRunningOperation()); + op->id = ++longRunningOperationsCount; + op->data = 10; + + // you need to hold the AsyncWebServerRequestPtr returned by pause(); + // This object is authorized to leave the scope of the request handler. + op->requestPtr = std::move(requestPtr); + + Serial.printf("[%u] Start long running operation for %" PRIu8 " seconds...\n", op->id, op->data); + longRunningOperations.push_back(std::move(op)); +} + +static bool processLongRunningOperation(LongRunningOperation *op) { + // request was deleted ? + if (op->requestPtr.expired()) { + Serial.printf("[%u] Request was deleted - stopping long running operation\n", op->id); + return true; // operation finished + } + + // processing the operation + Serial.printf("[%u] Long running operation processing... %" PRIu8 " seconds left\n", op->id, op->data); + + // check if we have finished ? + op->data--; + if (op->data) { + // not finished yet + return false; + } + + // Try to get access to the request pointer if it is still exist. + // If there has been a disconnection during that time, the pointer won't be valid anymore + if (auto request = op->requestPtr.lock()) { + Serial.printf("[%u] Long running operation finished! Sending back response...\n", op->id); + request->send(200, "text/plain", String(op->id) + " "); + + } else { + Serial.printf("[%u] Long running operation finished, but request was deleted!\n", op->id); + } + + return true; // operation finished +} + +/// ========================================================== + +void setup() { + Serial.begin(115200); + +#ifndef CONFIG_IDF_TARGET_ESP32H2 + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // Add a middleware to see how pausing a request affects the middleware chain + server.addMiddleware([](AsyncWebServerRequest *request, ArMiddlewareNext next) { + Serial.printf("Middleware chain start\n"); + + // continue to the next middleware, and at the end the request handler + next(); + + // we can check the request pause state after the handler was executed + if (request->isPaused()) { + Serial.printf("Request was paused!\n"); + } + + Serial.printf("Middleware chain ends\n"); + }); + + // HOW TO RUN THIS EXAMPLE: + // + // 1. Open several terminals to trigger some requests concurrently that will be paused with: + // > time curl -v http://192.168.4.1/ + // + // 2. Look at the output of the Serial console to see how the middleware chain is executed + // and to see the long running operations being processed and resume the requests. + // + // 3. You can try close your curl command to cancel the request and check that the request is deleted. + // Note: in case the network is disconnected, the request will be deleted. + // + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + // Print a message in case the request is disconnected (network disconnection, client close, etc.) + request->onDisconnect([]() { + Serial.printf("Request was disconnected!\n"); + }); + + // Instruct ESPAsyncWebServer to pause the request and get a AsyncWebServerRequestPtr to be able to access the request later. + // The AsyncWebServerRequestPtr is the ONLY object authorized to leave the scope of the request handler. + // The Middleware chain will continue to run until the end after this handler exit, but the request will be paused and will not + // be sent to the client until send() is called later. + Serial.printf("Pausing request...\n"); + AsyncWebServerRequestPtr requestPtr = request->pause(); + + // start our long operation... + startLongRunningOperation(std::move(requestPtr)); + }); + + server.begin(); +} + +static uint32_t lastTime = 0; + +void loop() { + if (millis() - lastTime >= 1000) { + +#ifdef ESP32 + Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap()); + std::lock_guard lock(longRunningOperationsMutex); +#endif + + // process all long running operations + longRunningOperations.remove_if([](const std::unique_ptr &op) { + return processLongRunningOperation(op.get()); + }); + + lastTime = millis(); + } +} diff --git a/platformio.ini b/platformio.ini index 0f643dda8..1397b13f4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,6 +19,8 @@ lib_dir = . src_dir = examples/PerfTests ; src_dir = examples/RateLimit ; src_dir = examples/Redirect +; src_dir = examples/RequestContinuation +; src_dir = examples/RequestContinuationComplete ; src_dir = examples/ResumableDownload ; src_dir = examples/Rewrite ; src_dir = examples/ServerSentEvents diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index ca0bc66c0..642dfa8b0 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -179,6 +179,8 @@ typedef enum { typedef std::function AwsResponseFiller; typedef std::function AwsTemplateProcessor; +using AsyncWebServerRequestPtr = std::weak_ptr; + class AsyncWebServerRequest { using File = fs::File; using FS = fs::FS; @@ -192,8 +194,9 @@ class AsyncWebServerRequest { AsyncWebServerResponse *_response; ArDisconnectHandler _onDisconnectfn; - // response is sent - bool _sent = false; + bool _sent = false; // response is sent + bool _paused = false; // request is paused (request continuation) + std::shared_ptr _this; // shared pointer to this request String _temp; uint8_t _parseState; @@ -484,6 +487,21 @@ class AsyncWebServerRequest { #endif AsyncWebServerResponse *beginResponse_P(int code, const String &contentType, PGM_P content, AwsTemplateProcessor callback = nullptr); + /** + * @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. + * The middelware chain will continue to be processed until the end, but no response will be sent. + * To resume operations (send the request), the request must be retrieved from the weak pointer and a send() function must be called. + * AsyncWebServerRequestPtr is the only object allowed to exist the scope of the request handler. + * @warning This function should be called from within the context of a request (in a handler or middleware for example). + * @warning While the request is paused, if the client aborts the request, the latter will be disconnected and deleted. + * 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(). + */ + AsyncWebServerRequestPtr pause(); + + bool isPaused() const { + return _paused; + } + /** * @brief Get the Request parameter by name * diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 94aa5d73c..8295b270b 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -9,6 +9,8 @@ #define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '=')) +static void doNotDelete(AsyncWebServerRequest *) {} + using namespace asyncsrv; enum { @@ -75,6 +77,8 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c) } AsyncWebServerRequest::~AsyncWebServerRequest() { + _this.reset(); + _headers.clear(); _pathParams.clear(); @@ -691,7 +695,7 @@ void AsyncWebServerRequest::_runMiddlewareChain() { } void AsyncWebServerRequest::_send() { - if (!_sent) { + if (!_sent && !_paused) { // log_d("AsyncWebServerRequest::_send()"); // user did not create a response ? @@ -711,6 +715,18 @@ void AsyncWebServerRequest::_send() { } } +AsyncWebServerRequestPtr AsyncWebServerRequest::pause() { + if (_paused) { + return _this; + } + client()->setRxTimeout(0); + // this shared ptr will hold the request pointer until it gets destroyed following a disconnect. + // this is just used as a holder providing weak observers, so the deleter is a no-op. + _this = std::shared_ptr(this, doNotDelete); + _paused = true; + return _this; +} + size_t AsyncWebServerRequest::headers() const { return _headers.size(); } @@ -887,13 +903,22 @@ AsyncWebServerResponse *AsyncWebServerRequest::beginResponse_P(int code, const S } void AsyncWebServerRequest::send(AsyncWebServerResponse *response) { + // request is already sent on the wire ? if (_sent) { return; } + + // if we already had a response, delete it and replace it with the new one if (_response) { delete _response; } _response = response; + + // if request was paused, we need to send the response now + if (_paused) { + _paused = false; + _send(); + } } void AsyncWebServerRequest::redirect(const char *url, int code) {