-
Notifications
You must be signed in to change notification settings - Fork 120
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
Conversation
src/Web.cpp
Outdated
} | ||
|
||
static String tagIdToJsonStr(const char *key) { | ||
StaticJsonDocument<512> doc; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 {}; |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside is that the heap space that is the capacity of the vector is never reduced or freed.
And because of this, I'd advice against static here. The main issue is, that we do not have any idea, how many entries there are and we are basically reserving heap space (at the minimum sizeof(String) * [maxEntries]
, propably way more).
Also what you have to keep in mind is, that as soon as std::vector runs out of space in it's internal array, it reserves a new memory block on the heap with the factor of 1.5 / 2 (so if an array of 12 elements is full and another object is pushed, the vector will reserve max 24 entries before moving the memory). This is to ensure an O(n) for reallocations over lifetime of the object.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please always use snprintf
instead of sprintf
. sprintf
is considered dangerous, since it does not check against buffer overflows. snprintf should always be preferred, since it takes a buffer size (here maxLen) and checks that we never write more than the buffer.
nvsKeys.clear(); | ||
// Dumps all RFID-keys from NVS into key array | ||
listNVSKeys("rfidTags", &nvsKeys, DumpNvsToArrayCallback); | ||
if (nvsKeys.size() == 0) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks ok for me, we do not start the chunked response here (just send back an empty array and return)
src/Web.cpp
Outdated
} | ||
if (nvsIndex == nvsKeys.size()) { | ||
// finish | ||
len += sprintf(((char *) buffer + len), "]"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for not using sprintf but directly accessing the array
src/Web.cpp
Outdated
[](uint8_t *buffer, size_t maxLen, size_t index) { | ||
maxLen = maxLen >> 1; // some sort of bug with actual size available, reduce the len | ||
size_t len = 0; | ||
static String json; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String json
does not needs to be static.
static String json; | |
String json; |
Here you can also use index to detect the start of the chunked response to reset the variable nvsIndex
:
if (index==0) {
// first call for us, reset index
nvsIndex = 0;
}
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please always use snprintf
instead of sprintf
. sprintf
is considered dangerous, since it does not check against buffer overflows. snprintf should always be preferred, since it takes a buffer size (here maxLen) and checks that we never write more than the buffer.
src/Web.cpp
Outdated
return serializedJsonString; | ||
} | ||
|
||
uint16_t nvsIndex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move nvsIndex
either as a static variable into the lambda in line 1736 or at least make the variable static
(so that it does not pollute the global namespace).
src/Web.cpp
Outdated
// construct chunked repsonse | ||
nvsIndex = 0; | ||
AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", | ||
[](uint8_t *buffer, size_t maxLen, size_t index) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If nvsKeys
is not static any more, we have to capture the variable here so it's available in the lambda. To prevent a copy of the vector due to capture by value, we call std::move to transfer the variable into the lambda (capture by reference would result in undefined behaviour since the local variable will not exists any more when we are called).
[](uint8_t *buffer, size_t maxLen, size_t index) { | |
[nvsKeys = std::move(nvsKeys)](uint8_t *buffer, size_t maxLen, size_t index) { |
nvsKeys.clear(); | ||
// Dumps all RFID-keys from NVS into key array | ||
listNVSKeys("rfidTags", &nvsKeys, DumpNvsToArrayCallback); | ||
if (nvsKeys.size() == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks ok for me, we do not start the chunked response here (just send back an empty array and return)
src/Web.cpp
Outdated
} | ||
if (nvsIndex == nvsKeys.size()) { | ||
// finish | ||
len += sprintf(((char *) buffer + len), "]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for not using sprintf but directly accessing the array
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")); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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, "]"); |
There was a problem hiding this comment.
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.
Regarding the buffer copying, it's still calling for memory issues IMHO.
Needs to be converted to
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. |
Yes you are correct. It's probably easier if we introduce a local variable for the remaining capacity
From my point of view still better than getting a buffer overflow, overwriting unknown memory blocks after the buffer and having at best an unhandled exception at the next malloc/free call. We can also check for an error from snprintf by |
endpoint /rfid/ids returns only tag ids buffer check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, I've added some changes to the code (mostly cosmetic). The code works as expected with entry count of over 300 dummy cards.
src/Web.cpp
Outdated
return; | ||
} | ||
// get tag details or just an array of id's | ||
bool idsOnly = request->hasParam("ids"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment in line #460. Here we just grab the parameter
bool idsOnly = request->hasParam("ids"); | |
const bool idsOnly = request->hasParam("ids-only"); |
src/Web.cpp
Outdated
@@ -457,6 +457,7 @@ void webserverStart(void) { | |||
|
|||
// RFID | |||
wServer.on("/rfid", HTTP_GET, handleGetRFIDRequest); | |||
wServer.addRewrite(new OneParamRewrite("/rfid/ids", "/rfid?ids=true")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose, that the parameter ids
should be called something like ids-only
. We have a parapemeter id
already, so a mixup could easily happen.
wServer.addRewrite(new OneParamRewrite("/rfid/ids", "/rfid?ids=true")); | |
wServer.addRewrite(new OneParamRewrite("/rfid/ids-only", "/rfid?ids-only=true")); |
while (nvsIndex < nvsKeys.size()) { | ||
// write tags as long we have enough room | ||
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str(), idsOnly); | ||
if ((len + json.length()) >= maxLen) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be only >
, maxLen is the length of the buffer, not an index.
if ((len + json.length()) >= maxLen) { | |
if ((len + json.length()) > maxLen) { |
src/Web.cpp
Outdated
} | ||
return len; | ||
}); | ||
nvsKeys.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just saw, that we are accessing nvsKeys here. Since we called std::move on the variable, nvsKeys does not hold the array any more. As soons as the lambds is finished, the nvsKeys will go out of scope and it'll be destoryed.
nvsKeys.clear(); |
[nvsKeys = std::move(nvsKeys), idsOnly](uint8_t *buffer, size_t maxLen, size_t index) { | ||
maxLen = maxLen >> 1; // some sort of bug with actual size available, reduce the len | ||
size_t len = 0; | ||
String json; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can move nvsIndex into the lambda here. Also nvsIndex should have the same type as std::vector::size().
String json; | |
static size_t nvsIndex = 0; | |
String json; |
src/Web.cpp
Outdated
bool idsOnly = request->hasParam("ids"); | ||
|
||
std::vector<String> nvsKeys {}; | ||
static uint16_t nvsIndex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment on line 1747.
static uint16_t nvsIndex; |
|
||
std::vector<String> nvsKeys {}; | ||
static uint16_t nvsIndex; | ||
nvsKeys.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed since nvsKeys was just created (since it's not a static variable any more), so it's already empty.
nvsKeys.clear(); |
} | ||
// construct chunked repsonse | ||
nvsIndex = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see 1747
nvsIndex = 0; |
nvsIndex++; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are finished here, so we can clean up here. (see comment on line 1775 regarding nvsKeys)
nvsIndex++; | |
} | |
nvsIndex++; | |
} else if (nvsIndex > nvsKeys.size()) { | |
nvsIndex = 0; | |
return 0; | |
} |
src/Web.cpp
Outdated
// construct chunked repsonse | ||
nvsIndex = 0; | ||
AsyncWebServerResponse *response = request->beginChunkedResponse("application/json", | ||
[nvsKeys = std::move(nvsKeys), idsOnly](uint8_t *buffer, size_t maxLen, size_t index) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the return type of the lambda to size_t here (so that we can write return 0
).
[nvsKeys = std::move(nvsKeys), idsOnly](uint8_t *buffer, size_t maxLen, size_t index) { | |
[nvsKeys = std::move(nvsKeys), idsOnly](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { |
/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