Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/rfid endpoint - Serve with chunked response #280

Merged
merged 8 commits into from Dec 29, 2023

Conversation

tueddy
Copy link
Collaborator

@tueddy tueddy commented Dec 12, 2023

/rfid endpoint:

A static buffer of 8KB is currently used to list the RFID tags. If this buffer is full, the returned JSON is truncated and not all entries are displayed in the web interface. . This happens with me from approx. 60 entries (depends on path length). It could also lead to a low memory situation and, in the worst case, to a crash.

With this PR, a list is first created holding the keys only and later the details such as path and game mode are sent as a chunked response with a smaller buffer.
No changes in the delivered JSON except that it is always complete

src/Web.cpp Outdated
}

static String tagIdToJsonStr(const char *key) {
StaticJsonDocument<512> doc;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't 512 bytes a bit large for a single tag id?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single tag id looks like:

{ "id": "003108198106", "fileOrUrl": "/Die Schönsten Lieder Zum Einschlafen", "playMode": 3, "lastPlayPos": 1842837, "trackLastPlayed": 0 }

maxLen of "fileOrUrl" is MAX_FILEPATH_LENTGH = 256, size calculation with https://arduinojson.org/v6/assistant gives about 350 Bytes needed. Recommended is power of 2, so 512 Bytes is best size.

src/Web.cpp Outdated
}
static std::vector<String> nvsKeys {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This vector will never shrink, just something to be aware of. Though it's good to avoid frequent allocations...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvsKeys is declared as local variable and i assume it gets out of scope after function handleGetRFIDRequest() finished.
Am i wrong here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's declared as static, meaning it lives as long as the entire program.
If you didn't add nvsKeys.clear() directly afterwards, I it should continue to grow with each call.

The upside is that push_back does not need to reserve more memory every time and instead can re-use the same heap space. The downside is that the heap space that is the capacity of the vector is never reduced or freed.

src/Web.cpp Outdated
if (nvsIndex == 0) {
// start, write first tag
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str());
len += sprintf(((char *) buffer), "[%s", json.c_str());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if maxlen < json len? Can we handle that case? We should make sure to never write more bytes to buffer than allowed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: put the first tag also in the loop, then you won't forget the size check too

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!
I never saw a buffer smaller than 1500 Bytes so JSON length never exceeds.
But i will do an additional check here for code safety.

nvsKeys.clear();
// Dumps all RFID-keys from NVS into key array
listNVSKeys("rfidTags", &nvsKeys, DumpNvsToArrayCallback);
if (nvsKeys.size() == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would not need this special handling for size == 0 if in the chunked response there was no special handling for the first tag.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing the first tag there is no comma in JSON, it is starting with the second entry.
That's is the reason to handle the special case size==0 here and not even start a chunked response.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not starting the chunked response in the first place is probably a good idea.

src/Web.cpp Outdated
}
if (nvsIndex == nvsKeys.size()) {
// finish
len += sprintf(((char *) buffer + len), "]");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case: that single byte for ']' or ',' could be one byte more than maxLen.

Why not write '[' and ']' by themselves without sprintf?

 /rfid returns an array of tag-id keys
 /rfid/details returns an array of tag-ids and details. Optional GET param "id" to list only a single assignment.
src/Web.cpp Outdated
@@ -457,6 +457,7 @@ void webserverStart(void) {

// RFID
wServer.on("/rfid", HTTP_GET, handleGetRFIDRequest);
wServer.addRewrite(new OneParamRewrite("/rfid/details", "/rfid?details=true"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OneParamRewrite is usually for path parameters in the form of /path/{some_param} -> /path?some_param={some_param} because AsyncWebServer cannot handle path params natively.

Does this even work and do something? Even if it does, using the "normal" AsyncWebRewrite makes more sense: https://github.com/me-no-dev/ESPAsyncWebServer#param-rewrite-with-matching

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it works fine, do you have a concrete improved code here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't test it, but just replacing 'OneParamRewrite' with 'AsyncWebRewrite' should do the same thing.

src/Web.cpp Outdated
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str());
len += sprintf(((char *) buffer), "[%s", json.c_str());
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str(), withDetails);
if (json.length() > maxLen) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You check that, but 4 lines later you're writing json.length() + 1 bytes, so you should also check for json.length() + 1 available space

src/Web.cpp Outdated
nvsIndex++;
}
while (nvsIndex < nvsKeys.size()) {
// write tags as long we have enough room
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str());
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str(), withDetails);
if ((len + json.length()) > maxLen) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue, check is for 1 byte too few because of the comma

src/Web.cpp Outdated
nvsIndex++;
}
if (nvsIndex == nvsKeys.size()) {
// finish
len += sprintf(((char *) buffer + len), "]");
len += snprintf(((char *) buffer + len), maxLen, "]");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That byte is written without a length check at all.

@SZenglein
Copy link
Contributor

SZenglein commented Dec 13, 2023

Regarding the buffer copying, it's still calling for memory issues IMHO.
Most importantly, every code like this:

snprintf(((char *) buffer + len), maxLen, "...", ....);

Needs to be converted to

snprintf(((char *) buffer + len), maxLen-len, "...", ...);

that would already prevent any invalid memory access when writing the buffer. If the string doesn't fit, the json would still be invalid though.

@tueddy tueddy merged commit a1a35c7 into biologist79:dev Dec 29, 2023
10 checks passed
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.

None yet

2 participants