mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-03 22:21:59 +10:00
Add TemplateEngineAsyncWeb for safe ESPAsyncWebServer chunked responses
Chunk callbacks may see maxLen==0 or need another tick before progress; plain 0 ends the body and breaks browsers. Provide helpers that use RESPONSE_TRY_AGAIN, bounded template render retries, and disconnect cleanup. Update the README and async examples to use the shared wrapper.
This commit is contained in:
@@ -140,9 +140,12 @@ All public headers are re-exported from `TemplateEngine.h`, so typical sketches
|
||||
|
||||
### Async Streaming Pattern
|
||||
|
||||
When serving requests with ESPAsyncWebServer, give every request its own `TemplateContext` so chunked rendering cannot be corrupted by overlapping clients. Build and cache your `PlaceholderRegistry` once during setup, then share it across handlers. The same pattern powers the DeviceFramework web UI and the DFTE examples:
|
||||
When serving requests with ESPAsyncWebServer, include `TemplateEngineAsyncWeb.h` and give every request its own `TemplateContext` so chunked rendering cannot be corrupted by overlapping clients. Build and cache your `PlaceholderRegistry` once during setup, then share it across handlers. The same pattern powers the DeviceFramework web UI and the DFTE examples:
|
||||
|
||||
```cpp
|
||||
#include <TemplateEngine.h>
|
||||
#include <TemplateEngineAsyncWeb.h>
|
||||
|
||||
/** Global registry prepared during setup() */
|
||||
std::shared_ptr<PlaceholderRegistry> registry;
|
||||
|
||||
@@ -164,19 +167,9 @@ void streamTemplate(AsyncWebServerRequest* request, const char* rootTemplate) {
|
||||
|
||||
request->onDisconnect([ctx]() mutable { ctx.reset(); });
|
||||
|
||||
AsyncWebServerResponse* response = request->beginChunkedResponse(
|
||||
"text/html; charset=utf-8",
|
||||
[ctx](uint8_t* buffer, size_t maxLen, size_t) mutable -> size_t {
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t written = TemplateRenderer::renderNextChunk(*ctx, buffer, maxLen);
|
||||
if (!written || TemplateRenderer::isComplete(*ctx) || TemplateRenderer::hasError(*ctx)) {
|
||||
ctx.reset(); // prevent cross-request pollution
|
||||
}
|
||||
return written;
|
||||
});
|
||||
AsyncWebServerResponse* response =
|
||||
TemplateEngineAsyncWeb::beginSafeTemplateResponse(
|
||||
request, "text/html; charset=utf-8", ctx, 128);
|
||||
|
||||
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
response->addHeader("Pragma", "no-cache");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include <TemplateEngineAsyncWeb.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <DNSServer.h>
|
||||
@@ -219,25 +220,9 @@ void streamTemplate(AsyncWebServerRequest* request,
|
||||
ctx->setRegistry(registryPtr.get());
|
||||
TemplateRenderer::initializeContext(*ctx, rootTemplate);
|
||||
|
||||
request->onDisconnect([ctx]() mutable {
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
AsyncWebServerResponse* response = request->beginChunkedResponse(
|
||||
"text/html; charset=utf-8",
|
||||
[ctx](uint8_t* buffer, size_t maxLen, size_t /*index*/) mutable -> size_t {
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t written = TemplateRenderer::renderNextChunk(*ctx, buffer, maxLen);
|
||||
|
||||
if (!written || TemplateRenderer::isComplete(*ctx) || TemplateRenderer::hasError(*ctx)) {
|
||||
ctx.reset();
|
||||
}
|
||||
|
||||
return written;
|
||||
});
|
||||
AsyncWebServerResponse* response =
|
||||
TemplateEngineAsyncWeb::beginSafeTemplateResponse(
|
||||
request, "text/html; charset=utf-8", ctx, 128);
|
||||
|
||||
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
response->addHeader("Pragma", "no-cache");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include <TemplateEngineAsyncWeb.h>
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
#include <DNSServer.h>
|
||||
@@ -86,23 +87,9 @@ void streamTemplate(AsyncWebServerRequest* request,
|
||||
ctx->setRegistry(registryPtr.get());
|
||||
TemplateRenderer::initializeContext(*ctx, tpl);
|
||||
|
||||
request->onDisconnect([ctx]() mutable {
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
AsyncWebServerResponse* response = request->beginChunkedResponse(
|
||||
"text/html; charset=utf-8",
|
||||
[ctx](uint8_t* buffer, size_t maxLen, size_t) mutable -> size_t {
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t written = TemplateRenderer::renderNextChunk(*ctx, buffer, maxLen);
|
||||
if (!written || TemplateRenderer::isComplete(*ctx) || TemplateRenderer::hasError(*ctx)) {
|
||||
ctx.reset();
|
||||
}
|
||||
return written;
|
||||
});
|
||||
AsyncWebServerResponse* response =
|
||||
TemplateEngineAsyncWeb::beginSafeTemplateResponse(
|
||||
request, "text/html; charset=utf-8", ctx, 128);
|
||||
|
||||
response->addHeader("Cache-Control", "no-cache");
|
||||
request->send(response);
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#ifndef TEMPLATE_ENGINE_ASYNC_WEB_H
|
||||
#define TEMPLATE_ENGINE_ASYNC_WEB_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <memory>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "TemplateEngine.h"
|
||||
|
||||
namespace TemplateEngineAsyncWeb {
|
||||
|
||||
inline void yieldForChunkRetry() {
|
||||
#if defined(ARDUINO_ARCH_ESP8266)
|
||||
optimistic_yield(1000);
|
||||
#else
|
||||
yield();
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename ContextT>
|
||||
inline bool isTemplateTerminal(const ContextT& context) {
|
||||
return TemplateRenderer::isComplete(context) || TemplateRenderer::hasError(context);
|
||||
}
|
||||
|
||||
template <typename ContextT>
|
||||
inline size_t renderTemplateChunkWithRetries(ContextT& context,
|
||||
uint8_t* buffer,
|
||||
size_t maxLen,
|
||||
unsigned maxNoProgressRetries = 32) {
|
||||
if (maxLen == 0) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
for (unsigned attempt = 0; attempt < maxNoProgressRetries; ++attempt) {
|
||||
size_t written = TemplateRenderer::renderNextChunk(context, buffer, maxLen);
|
||||
if (written > 0 || isTemplateTerminal(context)) {
|
||||
return written;
|
||||
}
|
||||
|
||||
yieldForChunkRetry();
|
||||
}
|
||||
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
template <typename StateT, typename FillFn, typename IsDoneFn>
|
||||
AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
|
||||
const char* contentType,
|
||||
const std::shared_ptr<StateT>& sharedState,
|
||||
FillFn fill,
|
||||
IsDoneFn isDone) {
|
||||
request->onDisconnect([state = sharedState]() mutable {
|
||||
state.reset();
|
||||
});
|
||||
|
||||
return request->beginChunkedResponse(contentType,
|
||||
[state = sharedState, fill, isDone](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t {
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (maxLen == 0) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
size_t written = fill(*state, buffer, maxLen, index);
|
||||
if (written == RESPONSE_TRY_AGAIN) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (written > 0) {
|
||||
return written;
|
||||
}
|
||||
|
||||
if (isDone(*state)) {
|
||||
state.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
yieldForChunkRetry();
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename StateT, typename FillFn, typename IsDoneFn>
|
||||
AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
|
||||
const String& contentType,
|
||||
const std::shared_ptr<StateT>& sharedState,
|
||||
FillFn fill,
|
||||
IsDoneFn isDone) {
|
||||
request->onDisconnect([state = sharedState]() mutable {
|
||||
state.reset();
|
||||
});
|
||||
|
||||
return request->beginChunkedResponse(contentType,
|
||||
[state = sharedState, fill, isDone](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t {
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (maxLen == 0) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
size_t written = fill(*state, buffer, maxLen, index);
|
||||
if (written == RESPONSE_TRY_AGAIN) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (written > 0) {
|
||||
return written;
|
||||
}
|
||||
|
||||
if (isDone(*state)) {
|
||||
state.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
yieldForChunkRetry();
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename ContextT>
|
||||
AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request,
|
||||
const char* contentType,
|
||||
const std::shared_ptr<ContextT>& sharedContext,
|
||||
unsigned maxNoProgressRetries = 32) {
|
||||
return beginSafeChunkedResponse(
|
||||
request,
|
||||
contentType,
|
||||
sharedContext,
|
||||
[maxNoProgressRetries](ContextT& context, uint8_t* buffer, size_t maxLen, size_t /*index*/) -> size_t {
|
||||
return renderTemplateChunkWithRetries(context, buffer, maxLen, maxNoProgressRetries);
|
||||
},
|
||||
[](const ContextT& context) -> bool {
|
||||
return isTemplateTerminal(context);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename ContextT>
|
||||
AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request,
|
||||
const String& contentType,
|
||||
const std::shared_ptr<ContextT>& sharedContext,
|
||||
unsigned maxNoProgressRetries = 32) {
|
||||
return beginSafeChunkedResponse(
|
||||
request,
|
||||
contentType,
|
||||
sharedContext,
|
||||
[maxNoProgressRetries](ContextT& context, uint8_t* buffer, size_t maxLen, size_t /*index*/) -> size_t {
|
||||
return renderTemplateChunkWithRetries(context, buffer, maxLen, maxNoProgressRetries);
|
||||
},
|
||||
[](const ContextT& context) -> bool {
|
||||
return isTemplateTerminal(context);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace TemplateEngineAsyncWeb
|
||||
|
||||
#endif // TEMPLATE_ENGINE_ASYNC_WEB_H
|
||||
Reference in New Issue
Block a user