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
- 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.
- Open webapp
2026.05.26 and log in.
- During init,
loadRemoteConversations → filterLoadedConversations → filterDeletedConnectionRequests builds { id, domain: undefined } for that conversation and passes it to matchQualifiedIds.
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.
App stuck on "Fetching your connections and conversations" —
matchQualifiedIdsthrows on legacy conversations with an undefineddomainEnvironment
2026.05.26.08.40.42com.wearezeta.zclient.macv3.40.5442) loadinghttps://app.wire.comapp.wire.comfor any account whose local IndexedDB contains pre-federation conversation recordswire.com)Summary
On startup, the app hangs forever on the "Fetching your connections and conversations" spinner. The cause is an uncaught
TypeErrorduring initial conversation load:(Function names from the minified bundle:
matchQualifiedIds=(0,F.a),ConversationRepository=we.)The rejection is never caught, so
initAppaborts and the conversation list never renders.Root cause
matchQualifiedIdsguards anulldomain but not anundefineddomain, then readsdomain.length:When either argument has
domain === undefined(notnull), the=== nullchecks are false and execution falls through toe.domain.length/t.domain.length, which throws.The undefined domain originates in the caller:
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 nodomainfield, so the constructed id is{ id, domain: undefined }, whichmatchQualifiedIdsthen throws on.Steps to reproduce
conversationsstore contains a CONNECT-type (type 3) conversation record with nodomainproperty and noqualified_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.2026.05.26and log in.loadRemoteConversations→filterLoadedConversations→filterDeletedConnectionRequestsbuilds{ id, domain: undefined }for that conversation and passes it tomatchQualifiedIds.matchQualifiedIdsreadsundefined.lengthand throws. The unhandled rejection abortsinitApp.Actual: app stays on the "Fetching your connections and conversations" spinner indefinitely.
Expected: legacy conversations without a
domainare handled gracefully and the conversation list loads.Notes
domain. The bug only surfaces against existing local data that predates thedomainfield.domain; in another, 827 of 827. Only CONNECT-type records reach the throwing path, but the missing-domaindata is broader.Suggested fix
Treat
undefineddomain the same asnullinmatchQualifiedIds, for example:Optionally, also normalize the constructed id in
filterDeletedConnectionRequestsso a missingdomaindefaults to the self/local backend domain rather thanundefined.Workaround (data side)
Setting
domainto 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.