@alteriom/painlessmesh 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,6 +39,7 @@
39
39
  #endif
40
40
  #include "painlessmesh/plugin.hpp"
41
41
  #include "painlessmesh/protocol.hpp"
42
+ #include "painlessmesh/validation.hpp"
42
43
 
43
44
  #include <functional>
44
45
  #include <map>
@@ -66,8 +67,13 @@ constexpr bool shouldFollowBridgeChannel(uint32_t localNodeId,
66
67
  * @brief One occurrence of the mesh SSID seen by an all-channel scan.
67
68
  */
68
69
  struct MeshChannelCandidate {
69
- uint8_t channel;
70
- int32_t rssi;
70
+ uint8_t channel = 0;
71
+ int32_t rssi = 0;
72
+ // `push_back({channel, rssi})` needs a constructor under gnu++11 (the
73
+ // ESP32 Arduino 2.x core), where a struct with default member
74
+ // initializers is not an aggregate.
75
+ MeshChannelCandidate() {}
76
+ MeshChannelCandidate(uint8_t channel_, int32_t rssi_) : channel(channel_), rssi(rssi_) {}
71
77
  };
72
78
 
73
79
  /**
@@ -884,13 +890,26 @@ class GatewayDataPackage : public plugin::SinglePackage {
884
890
  */
885
891
  bool requiresAck = false;
886
892
 
893
+ /**
894
+ * @brief Random value drawn once per sendToInternet() call
895
+ *
896
+ * The same on every attempt at that call, and part of its X-Request-Id and
897
+ * Idempotency-Key (requestIdFor()). messageId alone repeats: its counter is
898
+ * 16 bits, so a node that sends more than 65,535 requests in one boot
899
+ * reissues old ids, and a service that remembers keys would drop the new
900
+ * request as a repeat. 0 from a node that predates the field. JSON key
901
+ * "nonce", omitted when 0.
902
+ */
903
+ uint32_t requestNonce = 0;
904
+
887
905
  /**
888
906
  * @brief Number of additional JSON fields in this package
889
907
  *
890
908
  * Used for jsonObjectSize() calculation in ArduinoJson v6.
891
- * Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack = 9 fields
909
+ * Count: msgId, origin, ts, prio, dest_url, payload, content, retry, ack,
910
+ * nonce = 10 fields
892
911
  */
893
- static constexpr int numPackageFields = 9;
912
+ static constexpr int numPackageFields = 10;
894
913
 
895
914
  /**
896
915
  * @brief Default constructor
@@ -914,6 +933,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
914
933
  priority = jsonObj["prio"];
915
934
  retryCount = jsonObj["retry"];
916
935
  requiresAck = jsonObj["ack"] | false;
936
+ requestNonce = jsonObj["nonce"] | 0UL;
917
937
 
918
938
  #if ARDUINOJSON_VERSION_MAJOR < 7
919
939
  if (jsonObj.containsKey("dest_url"))
@@ -951,6 +971,7 @@ class GatewayDataPackage : public plugin::SinglePackage {
951
971
  jsonObj["content"] = contentType;
952
972
  jsonObj["retry"] = retryCount;
953
973
  jsonObj["ack"] = requiresAck;
974
+ if (requestNonce != 0) jsonObj["nonce"] = requestNonce;
954
975
  return jsonObj;
955
976
  }
956
977
 
@@ -988,30 +1009,89 @@ class GatewayDataPackage : public plugin::SinglePackage {
988
1009
  * @return A unique message ID
989
1010
  */
990
1011
  static uint32_t generateMessageId(uint32_t nodeId) {
991
- static uint16_t counter = 0;
1012
+ // The counter starts at a random point each boot. Starting at zero, the
1013
+ // first request after every reboot carried the same id as the first
1014
+ // request of the boot before -- and so the same X-Request-Id and
1015
+ // Idempotency-Key, which a service that remembers keys drops as a repeat.
1016
+ static uint16_t counter =
1017
+ static_cast<uint16_t>(validation::SecureRandom::generate());
992
1018
  ++counter;
1019
+ if (counter == 0) ++counter;
993
1020
  // Combine node ID (upper 16 bits) with counter (lower 16 bits)
994
1021
  return ((nodeId & 0xFFFF) << 16) | counter;
995
1022
  }
996
1023
 
997
1024
  };
998
1025
 
1026
+ /**
1027
+ * @brief HTTPClient transport errors, by what they say about the request
1028
+ *
1029
+ * HTTPClient::GET()/POST() return a negative code when the request failed
1030
+ * below HTTP. The ESP32 and ESP8266 cores number them the same way. What
1031
+ * matters to a retry is whether the request can have reached the server:
1032
+ * resending one that did delivers it twice, which for a request with an
1033
+ * effect -- a message, a payment, a counter -- is a second effect (the rig
1034
+ * showed a timed-out send issued four times).
1035
+ *
1036
+ * Where each is raised, in both cores' HTTPClient::sendRequest() for the
1037
+ * buffer GET()/POST() the gateway uses: -1, -2 and -3 before the request is
1038
+ * complete; everything else from handleHeaderResponse(), which runs after the
1039
+ * whole request was written -- so -4 is a connection that closed before the
1040
+ * reply began and -7 a reply that was not HTTP, both with the request already
1041
+ * at the server. -6 and -8 come from stream sends and from reading a body.
1042
+ */
1043
+ enum HttpTransportError : int {
1044
+ HTTP_TRANSPORT_CONNECTION_REFUSED = -1, ///< no connection was made
1045
+ HTTP_TRANSPORT_SEND_HEADER_FAILED = -2, ///< the request line never completed
1046
+ HTTP_TRANSPORT_SEND_PAYLOAD_FAILED = -3, ///< the body never completed
1047
+ HTTP_TRANSPORT_NOT_CONNECTED = -4, ///< closed before the reply began; sent
1048
+ HTTP_TRANSPORT_CONNECTION_LOST = -5, ///< dropped while reading the reply; sent
1049
+ HTTP_TRANSPORT_NO_STREAM = -6, ///< no stream to send or read with
1050
+ HTTP_TRANSPORT_NO_HTTP_SERVER = -7, ///< the reply was not HTTP; sent
1051
+ HTTP_TRANSPORT_TOO_LESS_RAM = -8, ///< out of memory sending or reading
1052
+ HTTP_TRANSPORT_ENCODING = -9, ///< the reply arrived malformed; sent
1053
+ HTTP_TRANSPORT_STREAM_WRITE = -10, ///< the reply could not be stored; sent
1054
+ HTTP_TRANSPORT_READ_TIMEOUT = -11, ///< sent, and no reply in time
1055
+ };
1056
+
1057
+ /**
1058
+ * @brief Can a request that failed with this transport error have reached
1059
+ * the server?
1060
+ *
1061
+ * True -- the safe answer -- for every code that is not known to fail before
1062
+ * the request was complete, including codes this library does not know.
1063
+ */
1064
+ inline bool transportErrorMayHaveReachedServer(int rawCode) {
1065
+ switch (rawCode) {
1066
+ case HTTP_TRANSPORT_CONNECTION_REFUSED:
1067
+ case HTTP_TRANSPORT_SEND_HEADER_FAILED:
1068
+ case HTTP_TRANSPORT_SEND_PAYLOAD_FAILED:
1069
+ return false;
1070
+ default:
1071
+ return true;
1072
+ }
1073
+ }
1074
+
999
1075
  /**
1000
1076
  * @brief The outcome of a gateway HTTP request, classified for the ack
1001
1077
  *
1002
1078
  * HTTPClient::GET()/POST() return an int with two distinct meanings: a
1003
1079
  * positive value is an HTTP status code from the destination, while a
1004
- * negative value is one of the client's own transport errors (HTTPC_ERROR_*,
1005
- * e.g. -1 for a refused connection or -11 for a read timeout). Zero is not
1006
- * produced by either.
1080
+ * negative value is one of the client's own transport errors (see
1081
+ * HttpTransportError). Zero is not produced by either.
1007
1082
  *
1008
1083
  * GatewayAckPackage::httpStatus is a uint16_t, so a negative code cannot be
1009
- * forwarded as-is. Transport errors are reported as httpStatus 0, which is
1010
- * what Mesh::handleGatewayAck() already treats as a retryable network error;
1011
- * the human-readable cause travels in GatewayAckPackage::error instead.
1084
+ * forwarded as-is. Transport errors are reported as httpStatus 0 with the
1085
+ * cause in GatewayAckPackage::error, and whether the origin node may resend
1086
+ * the request travels in GatewayAckPackage::retryable.
1087
+ *
1088
+ * The library applies HTTP's meaning of a status and nothing else. Whether a
1089
+ * particular service's reply means what the application wanted -- a message
1090
+ * really queued, a record really written -- is the application's decision,
1091
+ * made from the status and the response the result carries.
1012
1092
  */
1013
1093
  struct HttpRequestOutcome {
1014
- /** True only for status codes that indicate genuine delivery. */
1094
+ /** True only for 200, 201, 202 and 204. */
1015
1095
  bool success = false;
1016
1096
 
1017
1097
  /** Value to place in GatewayAckPackage::httpStatus (0 for transport errors). */
@@ -1021,39 +1101,64 @@ struct HttpRequestOutcome {
1021
1101
  bool transportError = false;
1022
1102
 
1023
1103
  /**
1024
- * True when the status was success-class but the response body said the
1025
- * service refused the request (issue #450).
1104
+ * True for a 2xx outside 200, 201, 202 and 204. 203 says a proxy
1105
+ * transformed the reply; 205, 206 and 208 are not what a request to an API
1106
+ * endpoint expects. The server answered, so it is never retried; it is
1107
+ * reported as a failure the application can inspect.
1026
1108
  */
1027
- bool refusedByBody = false;
1109
+ bool unverifiedStatus = false;
1028
1110
 
1029
1111
  /**
1030
- * True for a 2xx the gateway cannot vouch for: anything but 200, 201, 202
1031
- * and 204. CallMeBot answers HTTP 208 to a message that never arrives
1032
- * (issue #452), so a status outside the verified set is reported as a
1033
- * failure that carries the body, never as a delivery.
1112
+ * Whether resending the identical request is safe and useful: the request
1113
+ * cannot have reached the server, or the server said it did not take it
1114
+ * and to come back (429 Too Many Requests, 503 Service Unavailable). A
1115
+ * reply that may mean the request was processed -- any 2xx, a 500, a
1116
+ * gateway timeout, a read timeout -- is not retried, because a retry would
1117
+ * be a second copy of it.
1034
1118
  */
1035
- bool unverifiedStatus = false;
1119
+ bool retryable = false;
1036
1120
 
1037
1121
  /**
1038
- * One-line reason for a failure, taken from the response body when there
1039
- * is one; empty on success. Sized to fit a GatewayAckPackage::error.
1122
+ * One-line summary of the response body, for the error an application
1123
+ * reads; empty on success and when no body was read.
1040
1124
  */
1041
1125
  TSTRING reason;
1042
1126
  };
1043
1127
 
1044
- /** Longest response-body excerpt carried in an acknowledgment error. */
1045
- static const size_t GATEWAY_RESPONSE_REASON_MAX = 120;
1128
+ /**
1129
+ * Longest response-body summary carried in an acknowledgment. Long enough
1130
+ * for a service that repeats the request back before its verdict -- the
1131
+ * reply in issue #463 spent 97 of 120 characters on the echo and was cut
1132
+ * before the part that said why.
1133
+ */
1134
+ static const size_t GATEWAY_RESPONSE_REASON_MAX = 240;
1046
1135
 
1047
1136
  /**
1048
- * How much of a response body the gateway reads for classification and for
1049
- * the error it reports. Enough for a service's one-line verdict, small enough
1050
- * that an HTML error page cannot eat an ESP8266's heap.
1137
+ * How much of a response body the gateway keeps for the summary: its first
1138
+ * GATEWAY_RESPONSE_HEAD_BYTES and its last GATEWAY_RESPONSE_TAIL_BYTES.
1139
+ * Enough for a service's verdict at either end, small enough that an HTML
1140
+ * error page cannot eat an ESP8266's heap.
1051
1141
  */
1052
1142
  static const size_t GATEWAY_RESPONSE_HEAD_BYTES = 512;
1143
+ static const size_t GATEWAY_RESPONSE_TAIL_BYTES = 256;
1144
+
1145
+ /**
1146
+ * How far into a body the gateway reads looking for its end. Past this the
1147
+ * tail kept is the last of what was read, not the body's end; reading on
1148
+ * would hold the uplink for a reply nobody summarises.
1149
+ */
1150
+ static const size_t GATEWAY_RESPONSE_SCAN_BYTES = 8192;
1053
1151
 
1054
- /** How long the gateway waits for that head to arrive after the status. */
1152
+ /** How long the gateway reads the body for, after the status. */
1055
1153
  static const uint32_t GATEWAY_RESPONSE_HEAD_TIMEOUT_MS = 250;
1056
1154
 
1155
+ /**
1156
+ * Longest wait a server's Retry-After may impose before a retry. A longer one
1157
+ * is not honoured with a retry at all: the request fails and says when the
1158
+ * server asked to be retried, which is the application's call to make.
1159
+ */
1160
+ static const uint32_t GATEWAY_RETRY_AFTER_MAX_MS = 60000;
1161
+
1057
1162
  inline bool responseContains(const TSTRING& haystack, const char* needle) {
1058
1163
  #if defined(PAINLESSMESH_BOOST)
1059
1164
  return haystack.find(needle) != std::string::npos;
@@ -1065,13 +1170,15 @@ inline bool responseContains(const TSTRING& haystack, const char* needle) {
1065
1170
  /**
1066
1171
  * @brief Reduce a response body to one line fit for a log or an error string
1067
1172
  *
1068
- * Tags are dropped, whitespace collapsed and the result cut at maxLen with an
1069
- * ellipsis, so an HTML error page reads "Oops! Too many requests... You have
1070
- * called to the API to often." in a serial log instead of markup.
1173
+ * Tags are dropped and whitespace collapsed, so an HTML page reads as text in
1174
+ * a serial log. A body longer than maxLen keeps its beginning and its end,
1175
+ * joined by " ... ": services often lead with an echo of the request and
1176
+ * finish with the verdict, and a summary that keeps only the beginning keeps
1177
+ * the echo and loses the verdict (issue #463).
1071
1178
  */
1072
1179
  inline TSTRING summarizeResponseBody(
1073
1180
  const TSTRING& body, size_t maxLen = GATEWAY_RESPONSE_REASON_MAX) {
1074
- TSTRING out;
1181
+ TSTRING text;
1075
1182
  bool inTag = false;
1076
1183
  bool pendingSpace = false;
1077
1184
  for (size_t i = 0; i < body.length(); ++i) {
@@ -1090,48 +1197,252 @@ inline TSTRING summarizeResponseBody(
1090
1197
  pendingSpace = true;
1091
1198
  continue;
1092
1199
  }
1093
- if (pendingSpace && out.length() > 0) out += ' ';
1200
+ if (pendingSpace && text.length() > 0) text += ' ';
1094
1201
  pendingSpace = false;
1095
- out += c;
1096
- if (out.length() >= maxLen) {
1097
- out += "...";
1202
+ text += c;
1203
+ }
1204
+ if (text.length() <= maxLen) return text;
1205
+
1206
+ static const char JOIN[] = " ... ";
1207
+ const size_t joinLength = sizeof(JOIN) - 1;
1208
+ const size_t room = maxLen > joinLength ? maxLen - joinLength : 0;
1209
+ const size_t head = room / 2;
1210
+ const size_t tail = room - head;
1211
+ TSTRING out;
1212
+ for (size_t i = 0; i < head; ++i) out += text[i];
1213
+ out += JOIN;
1214
+ for (size_t i = text.length() - tail; i < text.length(); ++i) out += text[i];
1215
+ return out;
1216
+ }
1217
+
1218
+ /**
1219
+ * @brief The start and the end of a response body, read a byte at a time
1220
+ *
1221
+ * The gateway cannot keep a whole body, and a service may put its verdict at
1222
+ * either end: CallMeBot echoes the request first and says why last (#463), so
1223
+ * a reply of a kilobyte kept as its first 512 bytes had lost the part that
1224
+ * mattered before any summary ran. This keeps the first
1225
+ * GATEWAY_RESPONSE_HEAD_BYTES and a rolling last GATEWAY_RESPONSE_TAIL_BYTES,
1226
+ * and text() joins them with " ... " when bytes were dropped between.
1227
+ */
1228
+ class ResponseExcerpt {
1229
+ public:
1230
+ /** Keep one more byte; false once GATEWAY_RESPONSE_SCAN_BYTES were seen. */
1231
+ bool add(char c) {
1232
+ ++seen_;
1233
+ if (head_.length() < GATEWAY_RESPONSE_HEAD_BYTES) {
1234
+ head_ += c;
1235
+ } else {
1236
+ tail_[(tailStart_ + tailLength_) % GATEWAY_RESPONSE_TAIL_BYTES] = c;
1237
+ if (tailLength_ < GATEWAY_RESPONSE_TAIL_BYTES) {
1238
+ ++tailLength_;
1239
+ } else {
1240
+ tailStart_ = (tailStart_ + 1) % GATEWAY_RESPONSE_TAIL_BYTES;
1241
+ }
1242
+ }
1243
+ return seen_ < GATEWAY_RESPONSE_SCAN_BYTES;
1244
+ }
1245
+
1246
+ void add(const TSTRING& text) {
1247
+ for (size_t i = 0; i < text.length(); ++i) {
1248
+ if (!add(text[i])) return;
1249
+ }
1250
+ }
1251
+
1252
+ TSTRING text() const {
1253
+ TSTRING out = head_;
1254
+ if (seen_ > head_.length() + tailLength_) out += " ... ";
1255
+ for (size_t i = 0; i < tailLength_; ++i) {
1256
+ out += tail_[(tailStart_ + i) % GATEWAY_RESPONSE_TAIL_BYTES];
1257
+ }
1258
+ return out;
1259
+ }
1260
+
1261
+ private:
1262
+ TSTRING head_;
1263
+ char tail_[GATEWAY_RESPONSE_TAIL_BYTES] = {};
1264
+ size_t tailStart_ = 0;
1265
+ size_t tailLength_ = 0;
1266
+ size_t seen_ = 0;
1267
+ };
1268
+
1269
+ /**
1270
+ * @brief Removes HTTP/1.1 chunked transfer framing from a body read a byte at
1271
+ * a time, and passes the content on to a ResponseExcerpt
1272
+ *
1273
+ * HTTPClient's getString() decodes a chunked body but keeps all of it; the
1274
+ * gateway reads the raw stream to keep only a bounded excerpt, and the raw
1275
+ * stream is the framing: CallMeBot's real reply reached the application as
1276
+ * "a6 Message to: ... Message queued. ... 0" (hardware rig, 2026-09-15) --
1277
+ * the chunk size in front, the terminating zero-length chunk behind. This
1278
+ * reads the framing (hex size, optional ;extensions, CRLF, data, CRLF, ...,
1279
+ * 0, trailers) and forwards only the data. A malformed size line stops
1280
+ * decoding rather than guessing: what was decoded so far is kept.
1281
+ */
1282
+ class ChunkedBodyDecoder {
1283
+ public:
1284
+ explicit ChunkedBodyDecoder(ResponseExcerpt& excerpt) : excerpt_(excerpt) {}
1285
+
1286
+ /** One byte of the raw body; false once the body ended or the excerpt is
1287
+ * full, when the caller can stop reading. */
1288
+ bool add(char c) {
1289
+ switch (state_) {
1290
+ case State::Size:
1291
+ if (c == '\r') {
1292
+ state_ = State::SizeLf;
1293
+ } else if (c == ';') {
1294
+ state_ = State::Extension;
1295
+ } else if (c == ' ' || c == '\t') {
1296
+ // Tolerated around the size, as many servers emit it.
1297
+ } else {
1298
+ const int digit = hexValue(c);
1299
+ if (digit < 0 || sizeDigits_ >= 8) return stop();
1300
+ remaining_ = (remaining_ << 4) | static_cast<uint32_t>(digit);
1301
+ ++sizeDigits_;
1302
+ }
1303
+ return true;
1304
+ case State::Extension:
1305
+ if (c == '\r') state_ = State::SizeLf;
1306
+ return true;
1307
+ case State::SizeLf:
1308
+ if (c != '\n' || sizeDigits_ == 0) return stop();
1309
+ sizeDigits_ = 0;
1310
+ if (remaining_ == 0) {
1311
+ state_ = State::Done;
1312
+ return false;
1313
+ }
1314
+ state_ = State::Data;
1315
+ return true;
1316
+ case State::Data:
1317
+ --remaining_;
1318
+ if (remaining_ == 0) state_ = State::DataCr;
1319
+ if (!excerpt_.add(c)) {
1320
+ state_ = State::Done;
1321
+ return false;
1322
+ }
1323
+ return true;
1324
+ case State::DataCr:
1325
+ if (c != '\r') return stop();
1326
+ state_ = State::DataLf;
1327
+ return true;
1328
+ case State::DataLf:
1329
+ if (c != '\n') return stop();
1330
+ state_ = State::Size;
1331
+ return true;
1332
+ case State::Done:
1333
+ return false;
1334
+ }
1335
+ return false;
1336
+ }
1337
+
1338
+ void add(const TSTRING& raw) {
1339
+ for (size_t i = 0; i < raw.length(); ++i) {
1340
+ if (!add(raw[i])) return;
1341
+ }
1342
+ }
1343
+
1344
+ /** True when the terminating zero-length chunk was read. */
1345
+ bool complete() const { return state_ == State::Done && !malformed_; }
1346
+
1347
+ private:
1348
+ enum class State { Size, Extension, SizeLf, Data, DataCr, DataLf, Done };
1349
+
1350
+ static int hexValue(char c) {
1351
+ if (c >= '0' && c <= '9') return c - '0';
1352
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
1353
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
1354
+ return -1;
1355
+ }
1356
+
1357
+ bool stop() {
1358
+ malformed_ = true;
1359
+ state_ = State::Done;
1360
+ return false;
1361
+ }
1362
+
1363
+ ResponseExcerpt& excerpt_;
1364
+ State state_ = State::Size;
1365
+ uint32_t remaining_ = 0;
1366
+ uint8_t sizeDigits_ = 0;
1367
+ bool malformed_ = false;
1368
+ };
1369
+
1370
+ /** Whether a Transfer-Encoding header value names chunked encoding. */
1371
+ inline bool transferEncodingIsChunked(const TSTRING& value) {
1372
+ TSTRING lower;
1373
+ for (size_t i = 0; i < value.length(); ++i) {
1374
+ const char c = value[i];
1375
+ lower += (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
1376
+ }
1377
+ return responseContains(lower, "chunked");
1378
+ }
1379
+
1380
+ /**
1381
+ * @brief The delay a Retry-After header asks for, in milliseconds
1382
+ *
1383
+ * Only the delay-seconds form is read; an HTTP-date, or anything else, is
1384
+ * 0 -- no instruction -- rather than a guess. Values past
1385
+ * GATEWAY_RETRY_AFTER_MAX_MS are returned as they are, so the caller can see
1386
+ * the server asked for longer than it will wait.
1387
+ */
1388
+ inline uint32_t parseRetryAfterMs(const TSTRING& value) {
1389
+ uint64_t seconds = 0;
1390
+ size_t digits = 0;
1391
+ for (size_t i = 0; i < value.length(); ++i) {
1392
+ const char c = value[i];
1393
+ if (c == ' ' || c == '\t') {
1394
+ if (digits == 0) continue;
1098
1395
  break;
1099
1396
  }
1397
+ if (c < '0' || c > '9') return 0;
1398
+ seconds = seconds * 10 + static_cast<uint64_t>(c - '0');
1399
+ if (seconds > 0xFFFFFFFFULL / 1000ULL) return 0xFFFFFFFFUL;
1400
+ ++digits;
1100
1401
  }
1101
- return out;
1402
+ return digits == 0 ? 0 : static_cast<uint32_t>(seconds * 1000ULL);
1102
1403
  }
1103
1404
 
1104
1405
  /**
1105
- * @brief Does a success-class response body say the service refused the request?
1406
+ * @brief The identifier the gateway sends with a request, as both
1407
+ * X-Request-Id and Idempotency-Key
1106
1408
  *
1107
- * Some services answer a refusal with a 2xx. CallMeBot's WhatsApp API, which
1108
- * the sendToInternet example integrates, returns its "Too many requests" page
1109
- * under HTTP 201 and HTTP 203 (issue #450). The phrases are the ones seen
1110
- * from services this library's examples target, and they are deliberately
1111
- * specific: a JSON payload that merely contains the word "error" is not a
1112
- * refusal.
1409
+ * The same for every attempt at one sendToInternet() call, so a service that
1410
+ * honours Idempotency-Key treats a retry as the request it already has, and
1411
+ * anything recording requests can count the copies one call produced.
1412
+ *
1413
+ * The request's nonce keeps it unique beyond its messageId, whose counter
1414
+ * wraps after 65,535 requests in a boot. A package from a node that predates
1415
+ * the nonce (0) keeps the two-part form it always had.
1113
1416
  */
1114
- inline bool responseBodyRefuses(const TSTRING& body) {
1115
- static const char* const REFUSALS[] = {"Too many requests", "Oops!"};
1116
- for (const char* phrase : REFUSALS) {
1117
- if (responseContains(body, phrase)) return true;
1417
+ inline TSTRING requestIdFor(uint32_t originNode, uint32_t messageId,
1418
+ uint32_t requestNonce = 0) {
1419
+ char buffer[40];
1420
+ if (requestNonce == 0) {
1421
+ snprintf(buffer, sizeof(buffer), "pm-%08x-%08x",
1422
+ static_cast<unsigned>(originNode), static_cast<unsigned>(messageId));
1423
+ } else {
1424
+ snprintf(buffer, sizeof(buffer), "pm-%08x-%08x-%08x",
1425
+ static_cast<unsigned>(originNode), static_cast<unsigned>(messageId),
1426
+ static_cast<unsigned>(requestNonce));
1118
1427
  }
1119
- return false;
1428
+ return TSTRING(buffer);
1429
+ }
1430
+
1431
+ /** A request nonce: random, and never the 0 that means "none". */
1432
+ inline uint32_t newRequestNonce() {
1433
+ uint32_t nonce = validation::SecureRandom::generate();
1434
+ return nonce != 0 ? nonce : 1;
1120
1435
  }
1121
1436
 
1122
1437
  /**
1123
1438
  * @brief Classify an HTTPClient result for the gateway acknowledgment
1124
1439
  *
1125
- * A transport error (rawCode <= 0) is neither success nor an HTTP status.
1126
- * Only 200, 201, 202 and 204 count as delivery on the status alone, and even
1127
- * those are overturned by a body that says the service refused the request:
1128
- * CallMeBot answers "Too many requests" under 201 and 203 (issue #450). Any
1129
- * other 2xx is unverified. 203 means a proxy transformed the reply; 208 is
1130
- * what CallMeBot answers to a message that never arrives (issue #452); none
1131
- * of them is a delivery this library can vouch for, so they are failures
1132
- * that carry the body. Every failure's reason is a one-line excerpt of the
1133
- * body when there is one, so the origin node learns why and not only a
1134
- * number.
1440
+ * HTTP semantics only. 200, 201, 202 and 204 are a success. Any other 2xx is
1441
+ * an unverified failure the server answered; 1xx, 3xx, 4xx and 5xx are
1442
+ * failures. A retry is allowed only when it cannot produce a second copy of
1443
+ * the request: a transport error that failed before the request was complete,
1444
+ * or 429/503, where the server says it did not take the request. The reason
1445
+ * is a summary of whatever body was read, so the application learns why.
1135
1446
  *
1136
1447
  * @param rawCode The int returned by HTTPClient::GET() or ::POST()
1137
1448
  * @param body The start of the response body, empty if none was read
@@ -1142,22 +1453,16 @@ inline HttpRequestOutcome classifyHttpResult(int rawCode,
1142
1453
  HttpRequestOutcome outcome;
1143
1454
  if (rawCode <= 0) {
1144
1455
  outcome.transportError = true;
1456
+ outcome.retryable = !transportErrorMayHaveReachedServer(rawCode);
1145
1457
  return outcome;
1146
1458
  }
1147
1459
  outcome.ackStatus =
1148
1460
  static_cast<uint16_t>(rawCode > 0xFFFF ? 0xFFFF : rawCode);
1149
- const bool verified =
1461
+ outcome.success =
1150
1462
  rawCode == 200 || rawCode == 201 || rawCode == 202 || rawCode == 204;
1151
- if (verified && responseBodyRefuses(body)) {
1152
- outcome.refusedByBody = true;
1153
- outcome.reason = summarizeResponseBody(body);
1154
- return outcome;
1155
- }
1156
- outcome.success = verified;
1157
- if (!verified) {
1158
- outcome.unverifiedStatus = rawCode >= 200 && rawCode < 300;
1159
- outcome.reason = summarizeResponseBody(body);
1160
- }
1463
+ outcome.unverifiedStatus = !outcome.success && rawCode >= 200 && rawCode < 300;
1464
+ outcome.retryable = rawCode == 429 || rawCode == 503;
1465
+ if (!outcome.success) outcome.reason = summarizeResponseBody(body);
1161
1466
  return outcome;
1162
1467
  }
1163
1468
 
@@ -1394,13 +1699,40 @@ class GatewayAckPackage : public plugin::SinglePackage {
1394
1699
  */
1395
1700
  uint32_t timestamp = 0;
1396
1701
 
1702
+ /**
1703
+ * @brief The start of the response body, summarized to one line
1704
+ *
1705
+ * Carried on success as well as failure, because whether a reply means what
1706
+ * the application wanted is the application's decision, not the library's.
1707
+ * Empty when no body was read. JSON key "resp", omitted when empty.
1708
+ */
1709
+ TSTRING response = "";
1710
+
1711
+ /**
1712
+ * @brief Whether the origin node may resend the identical request
1713
+ *
1714
+ * 1 when the request cannot have reached the server or the server asked to
1715
+ * be retried, 0 when a resend could deliver it twice. -1 when the gateway
1716
+ * did not say -- one that predates the field -- and the origin node falls
1717
+ * back to its own reading of the status. JSON key "retry", omitted at -1.
1718
+ */
1719
+ int8_t retryable = -1;
1720
+
1721
+ /**
1722
+ * @brief How long the server asked to wait before a retry, in milliseconds
1723
+ *
1724
+ * From a Retry-After header on a 429 or 503. JSON key "retryAfter",
1725
+ * omitted when 0.
1726
+ */
1727
+ uint32_t retryAfterMs = 0;
1728
+
1397
1729
  /**
1398
1730
  * @brief Number of additional JSON fields in this package
1399
1731
  *
1400
1732
  * Used for jsonObjectSize() calculation in ArduinoJson v6.
1401
- * Count: msgId, origin, success, http, err, ts = 6 fields
1733
+ * Count: msgId, origin, success, http, err, ts, resp, retry, retryAfter = 9
1402
1734
  */
1403
- static constexpr int numPackageFields = 6;
1735
+ static constexpr int numPackageFields = 9;
1404
1736
 
1405
1737
  /**
1406
1738
  * @brief Default constructor
@@ -1427,10 +1759,19 @@ class GatewayAckPackage : public plugin::SinglePackage {
1427
1759
  #if ARDUINOJSON_VERSION_MAJOR < 7
1428
1760
  if (jsonObj.containsKey("err"))
1429
1761
  error = jsonObj["err"].as<TSTRING>();
1762
+ if (jsonObj.containsKey("resp"))
1763
+ response = jsonObj["resp"].as<TSTRING>();
1764
+ if (jsonObj.containsKey("retry"))
1765
+ retryable = jsonObj["retry"].as<bool>() ? 1 : 0;
1430
1766
  #else
1431
1767
  if (jsonObj["err"].is<TSTRING>())
1432
1768
  error = jsonObj["err"].as<TSTRING>();
1769
+ if (jsonObj["resp"].is<TSTRING>())
1770
+ response = jsonObj["resp"].as<TSTRING>();
1771
+ if (jsonObj["retry"].is<bool>())
1772
+ retryable = jsonObj["retry"].as<bool>() ? 1 : 0;
1433
1773
  #endif
1774
+ retryAfterMs = jsonObj["retryAfter"] | 0UL;
1434
1775
  }
1435
1776
 
1436
1777
  /**
@@ -1449,6 +1790,11 @@ class GatewayAckPackage : public plugin::SinglePackage {
1449
1790
  jsonObj["http"] = httpStatus;
1450
1791
  jsonObj["err"] = error;
1451
1792
  jsonObj["ts"] = timestamp;
1793
+ // Added in 2.1.0 and omitted when they say nothing, so an ack to a node
1794
+ // that predates them is the ack it always was.
1795
+ if (response.length() > 0) jsonObj["resp"] = response;
1796
+ if (retryable >= 0) jsonObj["retry"] = retryable == 1;
1797
+ if (retryAfterMs > 0) jsonObj["retryAfter"] = retryAfterMs;
1452
1798
  return jsonObj;
1453
1799
  }
1454
1800
 
@@ -1462,7 +1808,8 @@ class GatewayAckPackage : public plugin::SinglePackage {
1462
1808
  */
1463
1809
  size_t jsonObjectSize() const {
1464
1810
  // noJsonFields (from base class) + numPackageFields (our fields)
1465
- return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length();
1811
+ return JSON_OBJECT_SIZE(noJsonFields + numPackageFields) + error.length() +
1812
+ response.length();
1466
1813
  }
1467
1814
  #endif
1468
1815