Skip to content

fix: settings writes silently lost, and unknown keys panic the device - #48

Open
socquique wants to merge 2 commits into
jhoff:mainfrom
socquique:settings-robustness
Open

fix: settings writes silently lost, and unknown keys panic the device#48
socquique wants to merge 2 commits into
jhoff:mainfrom
socquique:settings-robustness

Conversation

@socquique

Copy link
Copy Markdown

1. Settings writes are lost, and the request still says "success"

JsonSettings shares one Preferences instance between the Arduino loop task and
the AsyncTCP task that serves the HTTP handlers, with no lock, and every accessor
opens and closes NVS around a single get or put:

int JsonSettings::getInt(const char *key) {
    preferences.begin(name, true);
    int value = preferences.getInt(key, this->find(key).intDefault);
    preferences.end();          // closes the shared handle
    return value;
}

loop() calls getMode() on every pass, so this runs thousands of times a second
while the web handlers write from another task. One task's end() closes the
handle the other is part way through: the read falls back to the compiled default,
the write is dropped, and the browser is told it succeeded.

Observed on an 8 module display: a POST /text returned success, mode read
back as 0 — the compiled default for that key — then reverted, and the display
never changed mode. A later POST /settings reported success without storing
anything and only took on the second attempt.

All accessors now take a recursive mutex; recursive because reset() calls
fromJson(toJson()) and all three take it. After the change, 12 of 12 consecutive
write-then-read-back cycles matched.

2. An unknown settings key panics the device

find() throws std::runtime_error for keys that are not in the settings map, and
fromJson() calls it on every key of the posted JSON with no try/catch
anywhere above it. The exception escapes on the web server task and takes the whole
device down.

Any browser holding a settings page from a different firmware version will post
keys this build has never heard of, which is enough to reboot the display from the
UI. Unknown keys are now logged and skipped.

Also releases the NVS handle on the validation-failure path, which returned without
calling end().

Testing

Built and run on an 8 module ESP32-C3 display. Mismatched-key case exercised by
posting a settings body containing a key the firmware does not define.

Touches JsonSettings.cpp alongside the <sstream> PR; trivial to rebase
whichever lands second.

socquique and others added 2 commits September 1, 2026 14:42
find() throws std::runtime_error for keys that are not in the settings
map, and fromJson() runs on the web server task with no try/catch around
it, so an uncaught exception panics the device. A browser still holding a
settings page from a different firmware version will post keys this build
has never heard of - which is enough to reboot the display from the UI.

Unknown keys are now logged and skipped. Also release the nvs handle on
the validation-failure path, which returned without calling end().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JsonSettings shares one Preferences instance between the Arduino loop
task and the AsyncTCP task that serves the HTTP handlers, with no lock,
and every accessor opens and closes NVS around a single get or put.
getMode() runs on every pass through loop(), so the window is wide open:
one task's end() closes the handle the other is part way through using.

The read then falls back to the compiled default and the write is simply
lost, while the request still answers "success" to the browser.

Reproduced on an 8 module display: a POST /text returned success, mode
read back as 0 (the compiled default for that key) and then reverted to
its previous value, and the display never changed mode. A later POST
/settings likewise reported success without storing anything; the same
request succeeded on the second attempt. After this change, 12 of 12
consecutive write-then-read-back cycles matched.

The mutex is recursive because reset() calls fromJson(toJson()) and all
three take it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant