Skip to content

Fix MUC join detection — accept presence with MUC xmlns when server skips id-echo - #73

Open
phwizard wants to merge 1 commit into
mainfrom
fix/muc-presence-join-detection
Open

Fix MUC join detection — accept presence with MUC xmlns when server skips id-echo#73
phwizard wants to merge 1 commit into
mainfrom
fix/muc-presence-join-detection

Conversation

@phwizard

Copy link
Copy Markdown
Member

Summary

Reimplements the fix from the original #63 on top of current main. The original PR's head is on a fork I can't push to, so this is a fresh PR from a dappros-owned branch carrying the same intent.

`presenceInRoom` used to confirm a MUC join only when a presence stanza arrived with `stanza.attrs.id === stanzaId` (our generated `presenceInRoom--` id). Ejabberd echoes the id, so the deployments we test on work fine — but some non-Ejabberd MUC servers (and a few older Ejabberd configs) drop the id from their self-presence response. On those servers the handler never fires → caller hits `presence_timeout` → treats it as join failure → messages get stuck in `pending` and the auto-resend loop keeps re-emitting them.

What changes

`matchesJoinConfirmation` now accepts either:

  1. Id-echo fast path (existing behavior, unchanged): `stanza.attrs.id === stanzaId` AND `from.startsWith(roomJID)`.
  2. MUC-xmlns fallback (new): `from = roomJID/` AND a child ``.

Path 2 is the standard XEP-0045 join-confirmation shape — every spec-compliant MUC server emits it on a successful join, even if it doesn't echo our `id`. The `from.includes('/')` check prevents accidental matches against bare-jid room broadcasts (only occupant self-presence has a resource).

What stays the same

All of main's accumulated improvements are preserved:

  • Unique-id generation (`presenceInRoom--`)
  • `isValidMucJid` upfront guard
  • Detailed error-code mapping (forbidden / not-allowed / remote-server-not-found / item-not-found)
  • `createTimeoutPromise`-driven timeout-reject — caller-visible behavior preserved, no silent best-effort downgrade

Error stanzas still gate-keep on `matchesJoinConfirmation` and reject via the existing mapping, so the error UX is unchanged.

lib/ stays in sync

Ran `npm run sync:lib` so the published artifact mirrors `src/`.

Why not silent best-effort

The original PR proposed returning `null` on timeout (best-effort join). That's a behavior change for every caller — it would silently downgrade "server didn't respond at all" from "throw" to "soft-fail with empty presence", masking real connectivity bugs. Keeping the timeout-reject preserves that signal; the new fallback path means it only fires on genuine failures, not on spec-compliant servers that don't id-echo.

Test plan

  • `npx vitest run` — existing 4 tests still pass (presenceInRoom has no direct unit tests yet; the test infrastructure work for that is on the qa-coverage track — PR Mirror Android L1 + L2 coverage to chat-component + wire testIds (+37 Vitest) #72).
  • Manual: deploy against an Ejabberd instance — id-echo path still works.
  • Manual: deploy against a server known to skip id-echo (or simulate via stripping the id from one outgoing presence response) — MUC-xmlns fallback should kick in.

Test plan against an actual deployment

Roman / Dmytro to confirm whether the "messages stuck as pending after a server change" symptom is still observed in production. If yes → this PR addresses it. If no (i.e. the unique-id strategy already sufficient on all observed servers) → can close in favor of leaving `#63` as historical context.

Supersedes

  • #63 — same intent, fresh rebase, dappros-side branch.

The presence-in-room handler used to resolve only when a presence
stanza arrived with `stanza.attrs.id === stanzaId` (our generated
`presenceInRoom-<ts>-<n>` id). Ejabberd echoes the id back so the
match works on the deployments we test on, but some non-Ejabberd
MUC servers (and a few older Ejabberd configs) drop the id from
their self-presence response. On those servers the handler never
fired → caller hit `presence_timeout` → treated it as join failure
→ messages got stuck in `pending` and the auto-resend loop kept
re-emitting them.

Reimplemented version of the fix from the original PR #63 (Feb 5),
rebased onto current main rather than the Feb-era code. Keeps every
improvement main has accumulated since:

- Unique-id generation (`presenceInRoom-<ts36>-<counter36>`)
- `isValidMucJid` upfront guard
- Detailed error-code mapping (forbidden / not-allowed /
  remote-server-not-found / item-not-found)
- `createTimeoutPromise`-driven timeout-reject (caller-visible
  behavior preserved — no silent best-effort downgrade)

What changes: `matchesJoinConfirmation` now accepts either
  (a) the existing id-echo fast path, OR
  (b) `from = roomJID/<resource>` + a MUC-namespace `<x>` child
      (i.e. an occupant self-presence per XEP-0045)
Option (b) is the standard XEP-0045 join-confirmation shape, so
spec-compliant servers without id-echo no longer hit the timeout
path on the happy join case.

The `from.includes('/')` check on the fallback prevents accidental
matches against the bare room jid (room broadcast, not occupant
self-presence). Error stanzas still gate-keep on the same
`matchesJoinConfirmation` and reject via the existing error-code
mapping, so the error UX is unchanged.

Also runs `npm run sync:lib` so the published artifact in `lib/`
stays in lockstep with `src/`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant