@alteriom/painlessmesh 1.9.12 → 1.9.13

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.
package/CHANGELOG.md CHANGED
@@ -19,6 +19,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
19
 
20
20
  - TBD
21
21
 
22
+ ## [1.9.13] - 2025-12-19
23
+
24
+ ### Fixed
25
+
26
+ - **HTTP 203 "Permanent Response" Issue with sendToInternet()** - Fixed issue where HTTP 203 (Non-Authoritative Information) responses from APIs like Callmebot WhatsApp appeared "permanent" without automatic recovery
27
+ - **Root Cause**: HTTP 203 responses (indicating cached/proxied responses) were correctly identified as failures but treated as terminal - requests were immediately removed from the pending queue without retry
28
+ - **Symptom**: User sees repeated "❌ Failed to send WhatsApp: Ambiguous response - HTTP 203..." messages with no automatic recovery, requiring manual intervention
29
+ - **Solution**: Modified `handleGatewayAck()` to implement intelligent retry logic for retryable failure types:
30
+ - HTTP 203 (Non-Authoritative Information) - cached/proxied responses, often temporary
31
+ - HTTP 5xx (Server Errors) - transient server issues (500, 502, 503, 504, etc.)
32
+ - HTTP 429 (Too Many Requests) - rate limiting with exponential backoff
33
+ - HTTP 0 (Network Errors) - connection failures, timeouts
34
+ - Non-retryable: HTTP 4xx client errors (except 429), HTTP 3xx redirects
35
+ - **Retry Behavior**: Uses exponential backoff (2s, 4s, 8s, 16s...) with configurable max retries (default: 3)
36
+ - **Testing**: Added comprehensive test coverage (50 assertions in 7 test cases) for retry classification and behavior
37
+ - **Documentation**: Added ISSUE_HTTP_203_RETRY_FIX.md with detailed analysis, examples, and HTTP 203 explanation
38
+ - **Impact**: Automatic recovery from temporary API caching issues, eliminating the "permanent" failure problem
39
+ - **API Compatibility**: Fully backward compatible, no breaking changes, existing code works unchanged
40
+
41
+ - **Hard Reset on Node - AsyncClient Deletion Race Condition** - Fixed ESP32/ESP8266 heap corruption crashes caused by race condition in deletion spacing logic during network disruptions
42
+ - **Root Cause**: The `scheduleAsyncClientDeletion()` function was updating `lastScheduledDeletionTime` at BOTH scheduling time (line 111) and execution time (line 130), creating a race condition where scheduler jitter could cause deletions to execute with insufficient spacing
43
+ - **Symptom**: Device crashes with "CORRUPT HEAP: Bad head at 0x40831d54. Expected 0xabba1234 got 0x4081fae4" even with 250ms spacing constant, particularly during network disruptions, TCP retries, or WiFi reconnection cycles
44
+ - **Race Condition Scenario**: When Task A scheduled for time T1 executes late at T1+jitter, it updates `lastScheduledDeletionTime` to T1+jitter, potentially AFTER Task B's scheduled time (which was calculated based on T1), causing Task B to execute with less than 250ms spacing
45
+ - **Solution**: Removed the execution-time update of `lastScheduledDeletionTime` in the task callback, relying solely on the scheduling-time update
46
+ - Ensures consistent, predictable spacing based on planned execution times
47
+ - Eliminates race condition where execution-time updates could "rewind" the timestamp
48
+ - Makes spacing calculation immune to scheduler jitter
49
+ - Guarantees minimum 250ms spacing between AsyncClient deletions in all scenarios
50
+ - **Why This Works**: By only updating at scheduling time, subsequent deletions are always spaced from the PREVIOUS deletion's planned time, not its actual execution time, providing conservative spacing guarantees even with scheduler jitter
51
+ - **Testing**: All test suites pass (1000+ assertions), including TCP retry (52), connection (3), and timing tests (7)
52
+ - **Documentation**: Added ISSUE_HARD_RESET_DELETION_RACE_FIX.md with detailed mathematical proof and race condition analysis
53
+ - **Impact**: Eliminates heap corruption crashes during network disruptions, enables stable operation through TCP retries and WiFi reconnection cycles
54
+
22
55
  ## [1.9.12] - 2025-12-18
23
56
 
24
57
  ### Fixed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div align="center">
6
6
 
7
- **Version 1.9.12** - Latest release with AsyncClient abort() timing fix for heap corruption prevention
7
+ **Version 1.9.13** - Latest release with HTTP 203 retry logic and AsyncClient deletion race condition fixes
8
8
 
9
9
  [![CI/CD Pipeline](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/ci.yml)
10
10
  [![Documentation](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml/badge.svg)](https://github.com/Alteriom/painlessMesh/actions/workflows/docs.yml)
package/library.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type": "git",
7
7
  "url": "https://github.com/Alteriom/painlessMesh"
8
8
  },
9
- "version": "1.9.12",
9
+ "version": "1.9.13",
10
10
  "frameworks": [
11
11
  "arduino"
12
12
  ],
@@ -1,5 +1,5 @@
1
1
  name=Alteriom PainlessMesh
2
- version=1.9.12
2
+ version=1.9.13
3
3
  author=Coopdis,Scotty Franzyshen,Edwin van Leeuwen,Germán Martín,Maximilian Schwarz,Doanh Doanh,Alteriom
4
4
  maintainer=Alteriom
5
5
  sentence=A painless way to setup a mesh with ESP8266 and ESP32 devices with Alteriom extensions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alteriom/painlessmesh",
3
- "version": "1.9.12",
3
+ "version": "1.9.13",
4
4
  "description": "painlessMesh is a user-friendly library for creating mesh networks with ESP8266 and ESP32 devices. This Alteriom fork includes additional packages for sensor data (SensorPackage), device commands (CommandPackage), and status monitoring (StatusPackage). It handles routing and network management automatically, so you can focus on your application. The library uses JSON-based messaging and syncs time across all nodes, making it ideal for coordinated behaviour like synchronized light displays or sensor networks reporting to a central node.",
5
5
  "keywords": [
6
6
  "arduino",
@@ -29,10 +29,10 @@
29
29
  /**
30
30
  * @brief AlteriomPainlessMesh library version information
31
31
  */
32
- #define ALTERIOM_PAINLESS_MESH_VERSION "1.9.12"
32
+ #define ALTERIOM_PAINLESS_MESH_VERSION "1.9.13"
33
33
  #define ALTERIOM_PAINLESS_MESH_VERSION_MAJOR 1
34
34
  #define ALTERIOM_PAINLESS_MESH_VERSION_MINOR 9
35
- #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 12
35
+ #define ALTERIOM_PAINLESS_MESH_VERSION_PATCH 13
36
36
 
37
37
  /**
38
38
  * @brief Library description and usage information
@@ -5,8 +5,8 @@
5
5
  * @file painlessMesh.h
6
6
  * @brief Main header file for Alteriom painlessMesh library
7
7
  *
8
- * @version 1.9.12
9
- * @date 2025-12-18
8
+ * @version 1.9.13
9
+ * @date 2025-12-19
10
10
  *
11
11
  * painlessMesh is a user-friendly library for creating mesh networks with
12
12
  * ESP8266 and ESP32 devices. This Alteriom fork includes additional packages
@@ -124,10 +124,10 @@ inline void scheduleAsyncClientDeletion(Scheduler* scheduler, AsyncClient* clien
124
124
  using namespace logger;
125
125
  Log(CONNECTION, "%s: Deferred cleanup of AsyncClient executing now\n", logPrefix);
126
126
 
127
- // Update the last deletion time when the deletion actually executes
128
- // This ensures subsequent deletions are spaced from the actual execution time,
129
- // not just the scheduled time, preventing concurrent cleanup operations
130
- lastScheduledDeletionTime = millis();
127
+ // Note: lastScheduledDeletionTime is updated at scheduling time (before this task runs), not here
128
+ // This ensures consistent spacing based on when deletions were scheduled, preventing
129
+ // the race condition where execution-time updates could "rewind" the timestamp
130
+ // and cause subsequent deletions to be scheduled too close together
131
131
 
132
132
  delete client;
133
133
  });
@@ -1503,13 +1503,63 @@ class Mesh : public ntp::MeshTime, public plugin::PackageHandler<T> {
1503
1503
 
1504
1504
  PendingInternetRequest& request = it->second;
1505
1505
 
1506
- // Call user callback
1507
- if (request.callback) {
1508
- request.callback(ack.success, ack.httpStatus, ack.error);
1506
+ // Check if this is a success response
1507
+ if (ack.success) {
1508
+ // Success - call callback and remove request
1509
+ if (request.callback) {
1510
+ request.callback(ack.success, ack.httpStatus, ack.error);
1511
+ }
1512
+ pendingInternetRequests.erase(it);
1513
+ return;
1514
+ }
1515
+
1516
+ // Failure response - determine if retryable
1517
+ bool isRetryable = false;
1518
+
1519
+ // HTTP 203 (Non-Authoritative Information) indicates cached/proxied response
1520
+ // This is often temporary and retrying may succeed when cache expires
1521
+ if (ack.httpStatus == 203) {
1522
+ isRetryable = true;
1523
+ Log(COMMUNICATION, "handleGatewayAck(): HTTP 203 detected, marking as retryable\n");
1524
+ }
1525
+ // HTTP 5xx server errors are typically transient
1526
+ else if (ack.httpStatus >= 500 && ack.httpStatus < 600) {
1527
+ isRetryable = true;
1528
+ Log(COMMUNICATION, "handleGatewayAck(): HTTP 5xx server error, marking as retryable\n");
1529
+ }
1530
+ // HTTP 429 (Too Many Requests) should be retried with backoff
1531
+ else if (ack.httpStatus == 429) {
1532
+ isRetryable = true;
1533
+ Log(COMMUNICATION, "handleGatewayAck(): HTTP 429 rate limit, marking as retryable\n");
1509
1534
  }
1535
+ // Network errors (httpStatus == 0) are retryable
1536
+ else if (ack.httpStatus == 0) {
1537
+ isRetryable = true;
1538
+ Log(COMMUNICATION, "handleGatewayAck(): Network error, marking as retryable\n");
1539
+ }
1540
+ // HTTP 4xx client errors (except 429) are NOT retryable
1541
+ // HTTP 3xx redirects are NOT retryable (should be followed by HTTPClient)
1542
+ // Other status codes are NOT retryable
1510
1543
 
1511
- // Remove from pending
1512
- pendingInternetRequests.erase(it);
1544
+ // If retryable and have retries left, schedule retry
1545
+ if (isRetryable && request.retryCount < request.maxRetries) {
1546
+ Log(COMMUNICATION, "handleGatewayAck(): Scheduling retry for msgId=%u (attempt %u/%u)\n",
1547
+ ack.messageId, request.retryCount + 1, request.maxRetries);
1548
+ scheduleInternetRetry(ack.messageId);
1549
+ } else {
1550
+ // Not retryable or max retries reached - call callback and remove
1551
+ if (request.retryCount >= request.maxRetries) {
1552
+ Log(ERROR, "handleGatewayAck(): Max retries reached for msgId=%u\n", ack.messageId);
1553
+ } else {
1554
+ Log(COMMUNICATION, "handleGatewayAck(): Non-retryable failure for msgId=%u (HTTP %u)\n",
1555
+ ack.messageId, ack.httpStatus);
1556
+ }
1557
+
1558
+ if (request.callback) {
1559
+ request.callback(ack.success, ack.httpStatus, ack.error);
1560
+ }
1561
+ pendingInternetRequests.erase(it);
1562
+ }
1513
1563
  }
1514
1564
 
1515
1565
  /**