Skip to content

Regression in 2026.05.26.08.40.42 #21409

@raphaelrobert

Description

@raphaelrobert

App stuck on "Fetching your connections and conversations" — matchQualifiedIds throws on legacy conversations with an undefined domain

Environment

  • webapp version: 2026.05.26.08.40.42
  • Observed in wire-desktop (macOS, Mac App Store build com.wearezeta.zclient.mac v3.40.5442) loading https://app.wire.com
  • Also reproducible in a browser against app.wire.com for any account whose local IndexedDB contains pre-federation conversation records
  • Backend: production (wire.com)

Summary

On startup, the app hangs forever on the "Fetching your connections and conversations" spinner. The cause is an uncaught TypeError during initial conversation load:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'length')
    at matchQualifiedIds
    at <anonymous>
    at Array.find (<anonymous>)
    at ConversationRepository.filterDeletedConnectionRequests
    at ConversationRepository.filterLoadedConversations
    at async ConversationRepository.loadRemoteConversations
    at async <init>.initApp

(Function names from the minified bundle: matchQualifiedIds = (0,F.a), ConversationRepository = we.)

The rejection is never caught, so initApp aborts and the conversation list never renders.

Root cause

matchQualifiedIds guards a null domain but not an undefined domain, then reads domain.length:

// util/QualifiedId.ts  (minified `i`, exported as F.a)
function matchQualifiedIds(e, t) {
  if (e === undefined || t === undefined) return false;
  const sameId = e.id === t.id;
  const sameDomain =
    e.domain === null || t.domain === null ||
    e.domain.length === 0 || t.domain.length === 0 ||   // <-- throws if domain is undefined
    e.domain === t.domain;
  return sameId && sameDomain;
}

When either argument has domain === undefined (not null), the === null checks are false and execution falls through to e.domain.length / t.domain.length, which throws.

The undefined domain originates in the caller:

// conversation/ConversationRepository.ts (minified)
filterDeletedConnectionRequests(conversations, /* found */ t, connections, /* deleted */ i = []) {
  for (const conversation of conversations) {
    const { type, qualified_id, id, domain } = conversation;
    type !== CONVERSATION_TYPE.CONNECT ||
      connections.find(c => matchQualifiedIds(c.conversationId, qualified_id || { id, domain }))
      ? /* keep */ : /* drop */;
  }
  ...
}

For a CONNECT-type conversation that has no qualified_id, it constructs { id, domain } from the conversation. Legacy records persisted by pre-federation clients have no domain field, so the constructed id is { id, domain: undefined }, which matchQualifiedIds then throws on.

Steps to reproduce

  1. Use an account whose local IndexedDB conversations store contains a CONNECT-type (type 3) conversation record with no domain property and no qualified_id (i.e. data written by a pre-federation webapp version). In our case the offending record was a pending connection request created in 2015.
  2. Open webapp 2026.05.26 and log in.
  3. During init, loadRemoteConversationsfilterLoadedConversationsfilterDeletedConnectionRequests builds { id, domain: undefined } for that conversation and passes it to matchQualifiedIds.
  4. matchQualifiedIds reads undefined.length and throws. The unhandled rejection aborts initApp.

Actual: app stays on the "Fetching your connections and conversations" spinner indefinitely.
Expected: legacy conversations without a domain are handled gracefully and the conversation list loads.

Notes

  • A fresh login in a clean browser does not reproduce, because the backend now returns conversations with a domain. The bug only surfaces against existing local data that predates the domain field.
  • In one affected account, 143 of 857 stored conversations had no domain; in another, 827 of 827. Only CONNECT-type records reach the throwing path, but the missing-domain data is broader.

Suggested fix

Treat undefined domain the same as null in matchQualifiedIds, for example:

const sameDomain =
  e.domain == null || t.domain == null ||   // == null covers null AND undefined
  e.domain.length === 0 || t.domain.length === 0 ||
  e.domain === t.domain;

Optionally, also normalize the constructed id in filterDeletedConnectionRequests so a missing domain defaults to the self/local backend domain rather than undefined.

Workaround (data side)

Setting domain to the user's backend domain on the affected local conversation records clears the crash and preserves history. This is only a per-client mitigation. The matcher guard above fixes it for everyone.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions