ParserStream::parse(Stream*, length) не обрабатывает частичное чтение Stream
Описание проблемы
В GSON/src/utils/ParserStream.h метод
bool parse(Stream* stream, size_t length)
предполагает, что вызов
stream->readBytes(json, length)
обязательно вернёт сразу все length байт.
На ESP32-S3 с сетевым Stream это предположение не всегда выполняется. readBytes() может вернуть только часть запрошенных данных, хотя соединение остаётся рабочим и остальные данные поступят позднее.
В результате ParserStream считает это ошибкой и полностью сбрасывает JSON.
Окружение
- МК: ESP32-S3 N16R8
- Arduino IDE
- ESP32 Arduino Core: 3.3.12
- Источник данных:
WiFiClientSecure
- GSON: текущая версия из Arduino Library Manager
- FastBot2: 1.2.8
Проблема обнаружилась при получении большого сообщения Telegram через FastBot2.
Как воспроизводится
HTTP-ответ Telegram имел:
HTTP/1.1 200 OK
Content-Length: 20804
При прямом чтении:
stream->readBytes(buffer, 20804);
один из тестов получил:
EXPECTED: 20804
RECEIVED: 15994
DIFFERENCE: 4810
При этом соединение не было разорвано, и оставшиеся данные можно было получить последующими операциями чтения.
Из-за этого текущая реализация:
bool parse(Stream* stream, size_t length) {
if (!stream || !length || !json.resize(length)) return 0;
if (stream->readBytes(json, length) != length) {
json.reset();
return 0;
}
return Parser::parse(json.buf(), length);
}
возвращает false.
Дополнительная проверка
Для проверки, что проблема именно в способе чтения, был сделан тест с последовательным чтением до получения полного размера.
После этого те же данные Telegram:
EXPECTED LENGTH: 20804
PARSE RESULT: OK
PARSER ELEMENTS: 18
RESPONSE SIZE: 20804
разбираются корректно.
Тест повторялся несколько раз и стабильно получал все 20804 байта.
Проверка через FastBot2
После изменения ParserStream полный путь:
Telegram
↓
WiFiClientSecure
↓
GyverHTTP
↓
StreamReader
↓
ParserStream
↓
GSON
↓
FastBot2
также начал работать корректно.
До исправления FastBot2 получал:
> got json
RAW RESPONSE LENGTH: 0
После исправления:
> got json
========== RAW CALLBACK ==========
RAW LENGTH: 20804
RAW FIRST: {"ok":true,"result":[...
==================================
Причём это успешно повторялось для нескольких больших сообщений.
Предлагаемое исправление
Вместо одного вызова readBytes() необходимо читать данные в цикле до получения всех length байт:
bool parse(Stream* stream, size_t length) {
if (!stream || !length || !json.resize(length)) return 0;
size_t received = 0;
while (received < length) {
if (!stream->available()) {
delay(1);
continue;
}
size_t available = stream->available();
size_t need = length - received;
size_t block = min(available, need);
size_t n = stream->readBytes((char*)json.buf() + received, block);
if (!n) {
json.reset();
return 0;
}
received += n;
}
return Parser::parse(json.buf(), length);
}
Этот вариант успешно проверен на реальном HTTP-ответе размером 20804 байта через WiFiClientSecure.
Важное замечание
В приведённом варианте отсутствует дополнительный timeout самого цикла чтения. Это сделано намеренно для минимального воспроизведения проблемы.
Возможно, в окончательной реализации следует использовать timeout исходного Stream, чтобы при обрыве соединения цикл не ожидал данные бесконечно.
Главная проблема, однако, заключается именно в предположении, что:
readBytes(buffer, length) == length
обязательно выполняется для сетевого Stream.
Для WiFiClientSecure это предположение не выполняется.
ParserStream::parse(Stream*, length)не обрабатывает частичное чтениеStreamОписание проблемы
В
GSON/src/utils/ParserStream.hметодпредполагает, что вызов
stream->readBytes(json, length)обязательно вернёт сразу все
lengthбайт.На ESP32-S3 с сетевым
Streamэто предположение не всегда выполняется.readBytes()может вернуть только часть запрошенных данных, хотя соединение остаётся рабочим и остальные данные поступят позднее.В результате
ParserStreamсчитает это ошибкой и полностью сбрасывает JSON.Окружение
WiFiClientSecureПроблема обнаружилась при получении большого сообщения Telegram через FastBot2.
Как воспроизводится
HTTP-ответ Telegram имел:
При прямом чтении:
один из тестов получил:
При этом соединение не было разорвано, и оставшиеся данные можно было получить последующими операциями чтения.
Из-за этого текущая реализация:
возвращает
false.Дополнительная проверка
Для проверки, что проблема именно в способе чтения, был сделан тест с последовательным чтением до получения полного размера.
После этого те же данные Telegram:
разбираются корректно.
Тест повторялся несколько раз и стабильно получал все 20804 байта.
Проверка через FastBot2
После изменения
ParserStreamполный путь:также начал работать корректно.
До исправления FastBot2 получал:
После исправления:
Причём это успешно повторялось для нескольких больших сообщений.
Предлагаемое исправление
Вместо одного вызова
readBytes()необходимо читать данные в цикле до получения всехlengthбайт:Этот вариант успешно проверен на реальном HTTP-ответе размером 20804 байта через
WiFiClientSecure.Важное замечание
В приведённом варианте отсутствует дополнительный timeout самого цикла чтения. Это сделано намеренно для минимального воспроизведения проблемы.
Возможно, в окончательной реализации следует использовать timeout исходного
Stream, чтобы при обрыве соединения цикл не ожидал данные бесконечно.Главная проблема, однако, заключается именно в предположении, что:
readBytes(buffer, length) == lengthобязательно выполняется для сетевого
Stream.Для
WiFiClientSecureэто предположение не выполняется.