Skip to content

Sender-key-only event overwrites the decrypted message under the same id — stored content lost, WAHA getChatMessage returns 404 #12

Description

@GalaxyRuler

Environment

  • WAHA devlikeapro/waha:gows-2026.8.2 → gows-plus v1.0.46 (c831525), whatsmeow fork v0.0.0-20260831053551-1ba57cc32ea6
  • SQLite message storage, one session, one group with LID addressing (addressing_mode="lid")
  • Also present on 2026.7.1 and 2026.8.1

Summary

A group message whose stanza carries a sender-key distribution (<enc type="pkmsg"> + <enc type="skmsg">) produces two events.Message with the same Info.ID: one whose Message holds only senderKeyDistributionMessage + messageContextInfo, and one with the real content. gows-plus upserts both under the same primary key, from separate goroutines, with last-writer-wins. When the sender-key-only event lands last, the decrypted content is replaced by the wrapper and is gone from storage — even though decryption succeeded and WAHA had already downloaded the media.

Where

  • whatsmeow message.go decryptMessages: iterates the <enc> children and calls handleDecryptedMessage once per child, so the pkmsg child (SKDM only) and the skmsg child (content) are dispatched as two events with identical Info.
  • src/gows/gows.go:
    func (gows *GoWS) handleEvent(event interface{}) {
        go gows.reissueEvent(event)
        go gows.storageEventHandler.handleEvent(event)
    }
  • src/gows/storage_event_handler.go handleSaveMessage → UpsertOneMessage for every event, no check whether the event has content.
  • src/storage/sqlstorage/tables.go MessageTable: OnConflict: ["id"], UpdateOnConflict: ["timestamp", "data"] → ON CONFLICT (id) DO UPDATE SET timestamp=…, data=… — a full replace of data.

Both goroutines race; the stanza order does not determine the storage order.

Evidence (production store, six weeks, one group)

  • 56 of 199 group messages are stored with Message = {senderKeyDistributionMessage, messageContextInfo} only, while Info.Type/Info.MediaType say text or document.
  • For the document ones, WAHA's MediaManager logged The message <id> has media, downloading it... → The media from '<id>' has been saved. at arrival — the content child decrypted and the media downloaded fine. The stored row lost it afterwards.
  • The is_real column (written only on INSERT in v1.0.46) vs. the blob's IsReal shows both orders happen:
    SELECT id, is_real, json_extract(data,'$.IsReal') AS blob_is_real,
           (SELECT group_concat(key) FROM json_each(json_extract(data,'$.Message'))) AS message_keys
    FROM gows_messages WHERE jid = '<group>@g.us'
      AND json_extract(data,'$.Message.senderKeyDistributionMessage') IS NOT NULL;
  • Trigger: a member's first message after being quiet for a while (WhatsApp attaches the SKDM). Any such message has roughly a coin-flip chance of being lost from storage.

Consequence in WAHA

GET /api/{session}/chats/{chat}/messages/{id} → getChatMessage → processIncomingMessage → shouldProcessIncomingMessage returns falsy for an SKDM-only message → NotFoundException('Message not found'). Anything that later needs the message by id — media re-download, quoting, listing — fails. In our case 14 documents became unrecoverable this way.

Relation to existing work

Suggested fix

Never let an event without content replace a row that has content. For example, in handleSaveMessage:

  • skip the upsert when !isRealMessage(event) and a row with that id already exists; or
  • guard the conflict update: ON CONFLICT (id) DO UPDATE SET … WHERE excluded.is_real = 1 OR gows_messages.is_real = 0.

Arguably the SKDM-only event should not be stored at all — WAHA drops it downstream in shouldProcessIncomingMessage anyway.

Happy to test a build against the affected store.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions