@alteriom/painlessmesh 2.0.3 → 2.1.0

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