github rmyndharis/OpenWA v0.8.11

latest releases: v0.23.3, v0.23.2, v0.23.1...
one month ago

Added

  • Prometheus counter for terminally-failed webhook deliveries. /api/metrics now exposes openwa_webhook_delivery_failures_total, incremented once per delivery that exhausts all its retries (mirroring the durable webhook_delivery_failures dead-letter record, on both the queued and the queue-disabled direct path). It is an in-process monotonic counter — cheap, real-time, and resetting only on restart, which Prometheus rate()/increase() treat as a normal counter reset — so a COUNT(*) over the retention-pruned failure table isn't queried per scrape. Operators can now alert on webhook failure rate from the scrape instead of tailing the structured log or querying the dead-letter endpoint.

Changed

  • Runtime feature flags are centralized in the config layer. AUTO_START_SESSIONS, STORE_EPHEMERAL_MESSAGES, RESOLVE_LID_TO_PHONE, SIMULATE_TYPING, and SIMULATE_TYPING_MAX_MS now resolve through a single discoverable features.* namespace on ConfigService (backed by one computeFeatureFlags() source of truth) instead of ad-hoc process.env reads scattered across the session and message services. Runtime behavior and defaults are unchanged. The four boolean flags are now validated at boot alongside the existing QUEUE_ENABLED/MCP_ENABLED/SERVE_DASHBOARD checks — ⚠️ behavior change: a deployment booting one of them with a non-canonical value (e.g. SIMULATE_TYPING=1, AUTO_START_SESSIONS=yes) will now fail fast naming the offending key until corrected to true/false/unset, instead of silently falling back to the default.
  • Coverage floors added for the session, webhook, and hook-manager modules. Per-directory Jest coverageThreshold entries now guard src/modules/session, src/modules/webhook, and src/core/hooks (set just below measured, matching the ratcheted floors already in place for the security, auth, engine-adapter, and integration modules), so a large deletion of these modules' tests fails CI instead of passing under the softer global gate.

Fixed

  • Inbound integration (ingress) deliveries now retry with backoff instead of failing on the first error. A queued ingress job was enqueued with no retry policy, so BullMQ ran a single attempt and a transient plugin-handler error (e.g. a 5xx from the sandbox) went straight to the dead-letter table — asymmetric with the webhook queue, which already retries. Ingress jobs now use bounded exponential-backoff retries (default 3 attempts, 5s base delay; configurable via INGRESS_MAX_ATTEMPTS and INGRESS_RETRY_DELAY_MS), and the dead-letter write still fires exactly once, only after the retries are exhausted. ⚠️ Ordering caveat: the per-conversation ordering lock guarantees no two same-conversation dispatches run concurrently, but a retried delivery re-enters the lock after its backoff and can therefore overtake a same-conversation successor that dispatched during the backoff window. Ingress order is best-effort regardless (the provider delivers over unordered HTTP); order-strict plugins must not assume a retried event still arrives in sequence. This trades strict order for throughput — BullMQ releases the worker slot during backoff, where retrying inside the lock would hold it.
  • /infra/status now actively probes the databases (SELECT 1) instead of trusting isInitialized. The Infrastructure panel's database tile read only DataSource.isInitialized, which stays true after a PostgreSQL backend dies (until an explicit .destroy()), so the tile showed the database healthy while it was actually down. The endpoint now runs a short, timeout-bounded SELECT 1 on both connections — the same probe /health/ready uses — so a post-init outage is reflected. SQLite is effectively always up, so this only changes behavior for a genuinely-down external PostgreSQL.
  • The settings panel reports the real docs and base-URL configuration. GET /settings hardcoded enableDocs: true (ignoring ENABLE_SWAGGER, so it claimed the API docs were enabled in production where they are disabled by default) and an http://localhost:<port> apiBaseUrl (ignoring the operator's configured BASE_URL). Both now reflect the real values, and autoReconnect reports the engine's actual default (on) rather than a hardcoded false for a config key that never existed.
  • A reaction no longer clobbers a message's delivery status. applyReaction read the full message row, mutated its reactions metadata, then wrote the whole row back — so a delivery ack (SENTDELIVERED/READ) that committed in the window between that read and write was overwritten with the stale status, permanently regressing the message's state until another ack happened to arrive. The reaction now writes only the metadata column via a scoped UPDATE keyed on (sessionId, waMessageId), so it can never touch status (acks and reactions update disjoint columns). The existing per-message serialization for concurrent reactions is unchanged.
  • PUT /infra/config returns the real HTTP status for a rejected configuration. A validation failure (an unknown engine type, or a value carrying a newline that would inject an extra env var) was caught and returned as HTTP 200 with { saved: false }, so a client branching on the status code alone treated a rejected save as success. Such validation errors now surface as their real 4xx (BadRequestException); a genuine persistence fault (e.g. a disk/permission error while writing the env file) still returns { saved: false } with 200, preserving the dashboard's body.saved handling.
  • Deleting a session no longer orphans its webhooks, templates, or stored Baileys messages on SQLite. webhooks, templates, and baileys_stored_messages declare an ON DELETE CASCADE foreign key to sessions, but the default data engine (SQLite) runs with foreign_keys OFF, so that cascade never fired — a session delete removed only the session, messages, and message_batches rows and left the rest behind indefinitely (orphaned webhooks rows in particular retain their signing secret and any custom headers). delete() now removes all CASCADE-FK child rows explicitly inside the same transaction (children before the parent), which is engine-agnostic — redundant-but-harmless on PostgreSQL, where the real cascade already handles it — and mirrors the ordering the data-restore path already uses.
  • The message:sending moderation gate and message:failed notification now cover every outbound path. Both hooks were wired only into the text sendText method, so a plugin registered on message:sending (the canonical pre-send moderation/compliance gate) saw no image/video/audio/document/sticker/location/contact/poll/reply/forward send and no bulk send at all, and a message:failed plugin saw only text-send failures. Every single sender now passes through a shared pre-send gate (a plugin can block or rewrite the payload) and a shared failure emitter, and BulkMessageService runs the same per-message gate (a block fails just that message, honoring stopOnError) and emits message:failed on a failed batch item. ⚠️ Behavior change: a message:sending plugin now receives — and can block/modify — media, extended, and bulk sends it previously never saw; the hook payload carries a type discriminator (image/video/poll/reply/… ) so a handler can scope its logic per send type, and its error field is sanitized so an SSRF-blocked media fetch does not expose the resolved internal address to plugins. A moderation block on a bulk item fails just that item (honoring stopOnError) without emitting message:failed, matching single-send where a block is a client error, not a delivery failure. message:sent is unchanged (still emitted once from the engine message_create path).
  • Sibling webhooks subscribed to the same event now get distinct idempotency keys. The per-event idempotency key was derived only from the event and its payload, so two different webhook endpoints registered for the same event on the same session received an identical X-OpenWA-Idempotency-Key. A receiver sitting behind both (or a shared dedup store) could drop the second endpoint's delivery as a replay of the first. The key is now salted with the destination webhook's id, so each endpoint is dedup'd independently while retries of the same delivery (including the queue-add→direct fallback) keep their stable key.
  • A session with auto-reconnect turned off no longer reports "reconnection failed after 0 attempts". When a disconnect fired with the reconnect budget set to 0 (auto-reconnect disabled), the session was marked FAILED with the exhausted-retries message and a count of 0 — implying a retry loop had run and failed rather than a feature that is simply off. That case now records an explicit "Auto-reconnect is disabled" reason; the genuine exhausted-retries message (with its real attempt count) is unchanged.
  • A failed inbound integration (ingress) delivery is no longer silently dropped on the inline path. With the queue disabled (or Redis unreachable), an ingress delivery is dispatched inline; if the plugin handler threw, the error was swallowed and the event was stranded — the provider had already received its 202, and the redrive tooling only scans the dead-letter table, which never got a row. The inline path now persists a dead-letter record (the same shape the queued path writes on its final failed attempt), so the event is redrivable. The write is best-effort: a failure to persist it is logged but never turns the 202 into a 5xx (the delivery is already dedup-persisted, so the provider won't re-send).
  • Plugin instance session bindings are re-derived on startup, so a binding lost while the plugin was momentarily unloaded self-heals. Provisioning a plugin instance mirrors its config into the plugin runtime (per-session config + activation) so an ingress handler resolves the right ctx.config; if the plugin happened to be unloaded at that moment the bridge was skipped (only an INFO audit), leaving the instance marked enabled but resolving base config only — previously recoverable only by re-saving the instance. A boot-time reconciliation now re-applies every enabled instance's binding from the persisted plugin_instances rows (honoring each row's real enabled flag), so a restart restores it. The binding logic moved into a dedicated service shared by provisioning and the reconciler.
  • Deleting a session now purges its on-disk engine auth directory. DELETE /sessions/:id tore down the running engine and removed the database rows but left the engine's persistent auth store behind (data/baileys/<name> for the Baileys engine, data/sessions/session-<name> for whatsapp-web.js) — these are keyed by session name and live independently of any engine instance, so recreating a session under the same name reloaded stale credentials. The delete now removes that directory too (best-effort, keyed by name, guarded against path traversal, and correctly a no-op when the session was already stopped and had no live engine). Thanks @m7fz7.

Don't miss a new OpenWA release

NewReleases is sending notifications on new releases.