fix: flush portal OTA response before reboot

This commit is contained in:
2026-09-17 15:08:10 +10:00
parent 4aab676574
commit f1fdeec8ee
2 changed files with 48 additions and 6 deletions
+6 -2
View File
@@ -1004,8 +1004,12 @@ void WiFiManagerHandlers::handleUpdateDone(AsyncWebServerRequest *request) {
}
sendApiJson(request, 200, jsonApiOtaUpdateSuccess());
delay(1000);
ESP.restart();
// AsyncWebServer queues this response; restarting from the request callback
// tears down its TCP connection before the client can receive the success
// JSON. Let the application's regular process() call perform the same
// delayed restart path used by the other restart-capable portal endpoints.
_wm->_rebootScheduled = true;
_wm->_rebootTime = millis() + _wm->REBOOT_DELAY_MS;
}
void WiFiManagerHandlers::sendApiJson(AsyncWebServerRequest *request, int code, const String& json) {
+42 -4
View File
@@ -46,6 +46,47 @@ async function requireRestartOutage(request) {
throw new Error('The portal never became unavailable after a successful OTA response.');
}
function waitForOtaResponse(page) {
// `waitForResponse()` alone waits until the enclosing test timeout when an
// embedded server resets the upload connection. Treat that as an immediate
// transport failure so a hardware artifact names the real fault instead of
// implying that the rendered form never submitted.
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
reject(new Error('Timed out waiting for the portal OTA POST /u response.'));
}, 90_000);
const isOtaRequest = (request) => {
const requestPath = new URL(request.url()).pathname;
return requestPath === '/u' && request.method() === 'POST';
};
const cleanup = () => {
clearTimeout(timeout);
page.off('response', onResponse);
page.off('requestfailed', onRequestFailed);
};
const onResponse = (response) => {
if (!isOtaRequest(response.request())) {
return;
}
cleanup();
resolve(response);
};
const onRequestFailed = (request) => {
if (!isOtaRequest(request)) {
return;
}
cleanup();
const failure = request.failure();
reject(new Error(`Portal OTA POST /u failed before a response: ${failure ? failure.errorText : 'unknown error'}`));
};
page.on('response', onResponse);
page.on('requestfailed', onRequestFailed);
});
}
test.describe('portal HTTP OTA contract', () => {
test('uploads B through the rendered portal form, requires automatic reboot, and observes B twice', async ({ page, request }) => {
test.skip(!firmware, 'OTA firmware is mounted only for portal-hardware ota.');
@@ -67,10 +108,7 @@ test.describe('portal HTTP OTA contract', () => {
await expect(input).toBeVisible();
await input.setInputFiles(firmware);
const updateResponse = page.waitForResponse((response) => {
const requestPath = new URL(response.url()).pathname;
return requestPath === '/u' && response.request().method() === 'POST';
});
const updateResponse = waitForOtaResponse(page);
await page.locator('#wm-ota-form button[type="submit"]').click();
const response = await updateResponse;