Accepting an email/phone invitation answers 200 before the DM rooms are created and the invitations are marked used, because `_processInvitation` is not awaited in `_processInvitationsToAddress`
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- backend-api-design
Research direction
Start in src/modules/legacy/tom-server/invitation-api/services/index.ts at _processInvitationsToAddress and compare its loop with the awaited processing at line 148. The issue suggests awaiting each _processInvitation call in sequence; service.test.ts can verify that accept() waits for the mocked database update. Done when the response waits for processing to finish and the test passes.
Written by the indexing model from the issue text.
Description
Severity: Minor — a bug in an edge case, a defect with a workaround, or hardening of a security-sensitive code path.
Summary
In _processInvitationsToAddress (services/index.ts:441) the loop calls this._processInvitation(...) without await. Accepting an invitation sent to an email or phone therefore returns 200 while the Matrix rooms and the accessed updates are still pending. If the accept link is requested again during that window, the same invitations are processed again and duplicate DM rooms are created. The try/catch around list and getInvitationStatus is redundant but harmless.
Root cause
The try blocks wrap calls that return promises, but the calls are not awaited, so the try has no effect on them.
Impact
- Who: a user who opens an invitation link (
GET /_twake/v1/invite/:id, behind cookie or bearer auth pluscheckInvitation) for an invitation that has arecipient(email or phone). That is the normal case forinvite, and forgenerateLinkwhen a recipient is given. - Effect 1: the response (or redirect) is sent before the direct-message rooms with the inviters exist. A client that syncs right away may not see them yet. They show up later through
/sync. - Effect 2: the invitation rows are flagged
accessedonly after two Matrix round-trips (createRoom, then the m.direct PUT). This is a GET that browsers may prefetch, reload or retry. A second request arriving in that window passes theaccessedcheck at line 141 and the!accessedfilter at line 524 again, so a secondcreateRoomruns for every pending invitation: the user gets duplicate DM rooms with each inviter. Awaiting does not make check-then-update atomic, but it shrinks the window and makes the response mean "done". - Not affected: process stability. There is no unhandled rejection, because
_processInvitationswallows all errors. Error reporting is unchanged too: per-invitation failures are only logged in either version.
Extent
src/modules/legacy/tom-server/invitation-api/services/index.ts:439-447: the un-awaited loop in_processInvitationsToAddress. It is reached only fromaccept()at :146, which is called fromcontrollers/index.ts:122(acceptInvitation) on the routeroutes/index.ts:261.- The awaited counterpart is at
services/index.ts:148(invitation without a recipient). - The redundant but harmless wrappers are at
services/index.ts:53-64(list) and:191-202(getInvitationStatus). Their helpers already log and rethrow (:383-390, :413-420). - No other un-awaited promise inside a try was found in this service. All the other private calls are awaited.
How to reproduce
- As user A,
POST /_twake/v1/invitewith{"contact":"[email protected]","medium":"email"}. As user C, send another invitation to the same address. - As B, open the link:
GET /_twake/v1/invite/<token>with B's access token. - The response arrives right away. Query
SELECT accessed FROM invitations WHERE recipient='[email protected]'immediately after it: the rows are still0, and they turn1only after the Synapse calls finish. - Send the same GET twice in parallel, or reload right away. Synapse receives two
POST /_matrix/client/v3/createRoomper inviter, and B ends up with two DM rooms with A and two with C.
In a unit test (service.test.ts), mock fetch with a delayed response. await service.accept(...) resolves before the mocked db.update is called.
Suggested fix
Await the processing in _processInvitationsToAddress. _processInvitation never rejects, so the inner try/catch can go. Process the invitations in sequence:
for (const invitation of invitations) {
await this._processInvitation(invitation, userId, authorization)
}
If the order does not matter, process them in parallel instead: await Promise.all(invitations.map((i) => this._processInvitation(i, userId, authorization))).
Optionally, make list and getInvitationStatus plain pass-throughs (return this._getUserInvitations(userId)) without the dead try/catch, or use return await if the extra log line is wanted.
To fully close the duplicate-room race, mark the invitation accessed with a conditional update (UPDATE ... SET accessed=1 WHERE id=? AND accessed=0) before calling Synapse, and skip it if no row was updated.
Occurrences (2)
-
src/modules/legacy/tom-server/invitation-api/services/index.ts:440inInvitationService— Consider using 'await' for the promise inside this 'try' or replace it with 'Promise.prototype.catch(...)' usage. (typescript:S4822)438 439 for (const invitation of invitations) { 440> try { 441 this._processInvitation(invitation, userId, authorization) 442 } catch (error) { -
src/modules/legacy/tom-server/invitation-api/services/index.ts:441inInvitationService— Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with thevoidoperator. (typescript:S9383)439 for (const invitation of invitations) { 440 try { 441> this._processInvitation(invitation, userId, authorization) 442 } catch (error) { 443 this.logger.error(
Found by twake-static-analysis (SonarQube typescript:S4822, typescript:S9383, grouped by root cause with Haiku and an Opus deep pass) on ToM-Server, commit c18c5b2765b5, severity: minor, 2 occurrence(s), first seen 2026-11-10.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 3
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 19
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from linagora/ToM-Server
-
bug claude Minor static-analysis
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
linagora/ToM-Server#125 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#123 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
linagora/ToM-Server#118 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
linagora/ToM-Server#117 ·
Maintainers usually reply within 1 day
-
ai-driven-fuzzing bug claude Minor
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
linagora/ToM-Server#114 ·
Maintainers usually reply within 1 day
All issues in linagora/ToM-Server
Similar issues
-
Add "append" and "prepend" slots for vue-vuetify extended renderersPossibly taken @simeyc claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
eclipsesource/jsonforms#2668 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
README の通知条件を実際の判定処理に合わせるPossibly taken @unhappychoice claimed this today. Openfactory:in-progress priority:medium
Difficulty 1/5 Under an hour Newbie friendliness 90/100
unhappychoice/oss-issue-opener#395 · 1 comment ·
Maintainers usually reply within 1 day
-
bug Website
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
OWASP/OWASP-Hunting#88 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day