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`

Open Beginner friendly
#126 0 comments 0 reactions 0 assignees View on GitHub

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

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

bug claude Minor static-analysis

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 plus checkInvitation) for an invitation that has a recipient (email or phone). That is the normal case for invite, and for generateLink when 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 accessed only 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 the accessed check at line 141 and the !accessed filter at line 524 again, so a second createRoom runs 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 _processInvitation swallows 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 from accept() at :146, which is called from controllers/index.ts:122 (acceptInvitation) on the route routes/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

  1. As user A, POST /_twake/v1/invite with {"contact":"[email protected]","medium":"email"}. As user C, send another invitation to the same address.
  2. As B, open the link: GET /_twake/v1/invite/<token> with B's access token.
  3. The response arrives right away. Query SELECT accessed FROM invitations WHERE recipient='[email protected]' immediately after it: the rows are still 0, and they turn 1 only after the Synapse calls finish.
  4. Send the same GET twice in parallel, or reload right away. Synapse receives two POST /_matrix/client/v3/createRoom per 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:440 in InvitationService — 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:441 in InvitationService — 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 the void operator. (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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from linagora/ToM-Server

All issues in linagora/ToM-Server

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.