Skip to content

Commit

Permalink
Request Continuation support
Browse files Browse the repository at this point in the history
**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:

#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.
  • Loading branch information
mathieucarbou committed Feb 1, 2025
1 parent 1b29f18 commit c638175
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 38 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,52 @@ myHandler.addMiddleware(&authMiddleware); // add authentication to a specific ha
These callbacks can be called multiple times during request parsing, so this is up to the user to now call the `AsyncAuthenticationMiddleware.allowed(request)` if needed and ideally when the method is called for the first time.
These callbacks are also not triggering the whole middleware chain since they are not part of the request processing workflow (they are not the final handler).

## Request Continuation

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 usage example can be found in the example called [RequestContinuation.ino](https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/examples/RequestContinuation/RequestContinuation.ino)

In the handler receiving the request, just excute:

```c++
AsyncWebServerRequestPtr ptr = request->pause();
```

This will pause the request and return a `AsyncWebServerRequestPtr` (this is a weak pointer).

**The AsyncWebServerRequestPtr is the ONLY object authorized to leave the scope of the request handler.**

Save somewhere this pointer and use it later to commit the response:

```c++
// you can check for expiration
if (requestPtr.expired()) {
// the request connection was closed some time ago so the request is not accessible anymore

} else if (longRunningTaskFinished) {
// this is what you always need to do when you want to access the request.
if (auto request = requestPtr.lock()) {
// send back the response
request->send(200, contentType, ...);

} else {
// the connection has been closed so the request is not accessible anymore
}
}
```

Most of the time you can simply it has:

```c++
if (auto request = requestPtr.lock()) {
// send back the response
request->send(200, contentType, ...);
}
```

## Original Documentation

<!-- no toc -->
Expand Down
170 changes: 170 additions & 0 deletions examples/RequestContinuation/RequestContinuation.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// 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 <Arduino.h>
#ifdef ESP32
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040)
#include <WebServer.h>
#include <WiFi.h>
#endif

#include <ESPAsyncWebServer.h>
#include <list>
#include <mutex>

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<LongRunningOperation *> longRunningOperations;
static size_t longRunningOperationsCount = 0;
static std::mutex longRunningOperationsMutex;

static void startLongRunningOperation(AsyncWebServerRequestPtr requestPtr) {
std::lock_guard<std::mutex> lock(longRunningOperationsMutex);

LongRunningOperation *op = new LongRunningOperation();
op->id = ++longRunningOperationsCount;
op->data = random(5, 20);

// you need to hold the AsyncWebServerRequestPtr returned by pause();
// This object is authorized to leave the scope of the request handler.
op->requestPtr = requestPtr;

Serial.printf("[%u] Start long running operation for %" PRIu8 " seconds...\n", op->id, op->data);
longRunningOperations.push_back(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("Long running operation ") + op->id + " finished.");

} 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 comand 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 ptr = request->pause();

// start our long operation...
startLongRunningOperation(ptr);
});

server.begin();
}

static uint32_t lastTime = 0;

void loop() {
if (millis() - lastTime >= 1000) {

#ifdef ESP32
Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap());
#endif

std::lock_guard<std::mutex> lock(longRunningOperationsMutex);

// process all long running operations
std::list<LongRunningOperation *> finished;
for (auto it = longRunningOperations.begin(); it != longRunningOperations.end(); ++it) {
bool done = processLongRunningOperation(*it);
if (done) {
finished.push_back(*it);
}
}

// remove finished operations
for (LongRunningOperation *op : finished) {
Serial.printf("[%u] Deleting finished long running operation\n", op->id);
longRunningOperations.remove(op);
delete op;
}

lastTime = millis();
}
}
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ lib_dir = .
src_dir = examples/PerfTests
; src_dir = examples/RateLimit
; src_dir = examples/Redirect
; src_dir = examples/RequestContinuation
; src_dir = examples/ResumableDownload
; src_dir = examples/Rewrite
; src_dir = examples/ServerSentEvents
Expand Down
23 changes: 21 additions & 2 deletions src/ESPAsyncWebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ typedef enum {
typedef std::function<size_t(uint8_t *, size_t, size_t)> AwsResponseFiller;
typedef std::function<String(const String &)> AwsTemplateProcessor;

using AsyncWebServerRequestPtr = std::weak_ptr<AsyncWebServerRequest>;

class AsyncWebServerRequest {
using File = fs::File;
using FS = fs::FS;
Expand All @@ -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<AsyncWebServerRequest> _this; // shared pointer to this request

String _temp;
uint8_t _parseState;
Expand Down Expand Up @@ -251,6 +254,8 @@ class AsyncWebServerRequest {
void _handleUploadByte(uint8_t data, bool last);
void _handleUploadEnd();

void _send();

public:
File _tempFile;
void *_tempObject;
Expand Down Expand Up @@ -478,6 +483,20 @@ 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.
* @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, any network disconnection will cause the request to be 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
*
Expand Down
Loading

0 comments on commit c638175

Please sign in to comment.