Skip to content

fix: handle federation events for banned/invited users#384

Closed
ggazzo wants to merge 5 commits intomainfrom
fix/ban-invited-user-federation
Closed

fix: handle federation events for banned/invited users#384
ggazzo wants to merge 5 commits intomainfrom
fix/ban-invited-user-federation

Conversation

@ggazzo
Copy link
Copy Markdown
Member

@ggazzo ggazzo commented Mar 24, 2026

Summary

Fixes two related issues where our homeserver fails to properly route and process federation events involving banned or invited users.

Bug 1: Banning a user who has a pending invite

When a user is banned before accepting a federation invite, the remote server cannot process the ban event because it has no room state (only an outlier invite event). The SDK's validateEvent requires the m.room.create event to determine the room version, which doesn't exist on a server that never joined the room. The ban event is silently discarded and the invite gets stuck in the UI.

Fix: Emit a new homeserver.matrix.membership.rejected event when validateEvent fails with UnknownRoomError for membership events, so the application layer can clean up the stale invite.

Bug 2: Re-inviting a user after ban+unban cycle

After banning and unbanning a user, re-inviting them fails with a 500 error. getServerSetInRoom only includes servers with join membership when computing event destinations. After a ban, the user's membership changes to ban, so their server is excluded from the destination set. The unban (leave) event is never delivered to the remote server, breaking the state chain. When a new invite arrives, the remote server cannot resolve prev_events it never received.

Fix: Include ban and invite memberships in getServerSetInRoom so these servers continue receiving room events.

Documentation

Added docs/partial-room-state-for-invites.md documenting both issues, the gap compared to Synapse (which stores partial room state from invite_room_state), and a proposed long-term solution.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed federation scenarios where room invites could remain stuck in the UI due to partial room state handling
    • Improved server routing for membership state changes to ensure events are properly delivered to all federated servers
  • Documentation

    • Added documentation describing federated room edge cases and proposed alignment patterns

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

Walkthrough

Adds documentation describing two federated Matrix-room edge cases and implements corresponding fixes: (1) emitting membership rejection events when m.room.member validation fails due to missing room state, and (2) extending server set routing to include servers with users in ban and invite membership states.

Changes

Cohort / File(s) Summary
Documentation
docs/partial-room-state-for-invites.md
New document describing two edge cases: partial room state handling for pending invites and server set routing inconsistencies after bans, including proposed solutions.
Event Signature Type
packages/federation-sdk/src/index.ts
Added 'homeserver.matrix.membership.rejected' event signature to HomeserverEventSignatures with event and reason properties.
Event Validation Error Handling
packages/federation-sdk/src/services/event.service.ts
Modified processIncomingPDUs to emit membership rejection events when validateEvent throws UnknownRoomError for m.room.member events, then skip further processing.
Server Routing Logic
packages/federation-sdk/src/services/state.service.ts, packages/federation-sdk/src/services/state.service.spec.ts
Extended getServerSetInRoom to include servers with users in ban and invite membership states alongside join, with corresponding test expectations updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: fixing federation event handling for banned and invited users by emitting rejection signals and expanding server routing criteria.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ggazzo added 3 commits March 24, 2026 13:12
…vited users

When a server receives a membership event (e.g., ban) for a room where only
an invite exists, event validation fails because no m.room.create event is
stored locally. Previously this silently discarded the event, leaving the
invite stuck.

Now emits a `homeserver.matrix.membership.rejected` event so consumers can
handle the case (e.g., clean up the pending invite).

Includes documentation on the partial room state gap compared to Synapse.
After banning a user, their server is excluded from event destinations
because getServerSetInRoom only considers 'join' membership. This causes
the unban (leave) event to never reach the remote server, breaking the
state chain and preventing re-invites.
getServerSetInRoom only considered servers with 'join' membership,
excluding servers whose users were banned or had pending invites.
This caused unban (leave) events to never reach the banned user's
server, breaking the state chain and preventing re-invites.

Include 'ban' and 'invite' memberships so these servers continue
receiving room events needed to maintain a consistent state.
@ggazzo ggazzo force-pushed the fix/ban-invited-user-federation branch from dc3addc to 91e5cc4 Compare March 24, 2026 16:12
The test previously asserted that banned and invited servers were
excluded from the destination set. Updated to match the new behavior
where these servers are included to ensure they receive room events.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 12.50000% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.83%. Comparing base (42cf7d8) to head (288a7a2).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...kages/federation-sdk/src/services/event.service.ts 0.00% 13 Missing ⚠️
...kages/federation-sdk/src/services/state.service.ts 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #384      +/-   ##
==========================================
+ Coverage   50.37%   50.83%   +0.46%     
==========================================
  Files          99      101       +2     
  Lines       11336    11477     +141     
==========================================
+ Hits         5710     5834     +124     
- Misses       5626     5643      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sampaiodiego sampaiodiego marked this pull request as ready for review March 24, 2026 22:20
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/federation-sdk/src/services/state.service.ts">

<violation number="1" location="packages/federation-sdk/src/services/state.service.ts:700">
P1: `getServerSetInRoom` now includes `ban`/`invite` servers globally, which broadens EDU fanout and access authorization to non-joined servers.</violation>
</file>

<file name="packages/federation-sdk/src/services/event.service.ts">

<violation number="1" location="packages/federation-sdk/src/services/event.service.ts:171">
P1: Do not let membership-rejection handler failures abort PDU processing; guard the emitted hook so validation failures still get skipped.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


const servers = new Set<string>();

const residentMemberships = new Set(['join', 'invite', 'ban']);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: getServerSetInRoom now includes ban/invite servers globally, which broadens EDU fanout and access authorization to non-joined servers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/federation-sdk/src/services/state.service.ts, line 700:

<comment>`getServerSetInRoom` now includes `ban`/`invite` servers globally, which broadens EDU fanout and access authorization to non-joined servers.</comment>

<file context>
@@ -697,8 +697,10 @@ export class StateService {
 
 		const servers = new Set<string>();
 
+		const residentMemberships = new Set(['join', 'invite', 'ban']);
+
 		for (const event of state.values()) {
</file context>
Fix with Cubic

Comment on lines +171 to +174
await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
event,
reason: err.message,
});
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Do not let membership-rejection handler failures abort PDU processing; guard the emitted hook so validation failures still get skipped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/federation-sdk/src/services/event.service.ts, line 171:

<comment>Do not let membership-rejection handler failures abort PDU processing; guard the emitted hook so validation failures still get skipped.</comment>

<file context>
@@ -167,12 +167,19 @@ export class EventService {
-							err,
-						});
+						if (err instanceof Error && err.name === 'UnknownRoomError' && event.type === 'm.room.member') {
+							await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
+								event,
+								reason: err.message,
</file context>
Suggested change
await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
event,
reason: err.message,
});
try {
await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
event,
reason: err.message,
});
} catch (emitErr) {
this.logger.error({
msg: 'Failed to emit membership rejection event',
origin,
event,
err: emitErr,
});
}
Fix with Cubic

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
docs/partial-room-state-for-invites.md (1)

5-26: Optional wording polish: prefer “invitation” in prose.

For formal documentation text, “invitation” reads cleaner than noun-form “invite” (keep protocol field names like invite_room_state unchanged).

Also applies to: 74-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/partial-room-state-for-invites.md` around lines 5 - 26, The document
uses the noun "invite" in prose; change these prose occurrences to "invitation"
while keeping protocol field and event names unchanged (e.g., preserve
`invite_room_state`, `m.room.create`, `m.room.join_rules` as-is). Update all
prose mentions throughout the file (including the section describing Synapse
behavior and any other occurrences) to use "invitation" for readability, but do
not modify code/protocol identifiers or example event names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/federation-sdk/src/services/event.service.ts`:
- Around line 170-175: The UnknownRoomError branch currently calls
this.eventEmitterService.emit('homeserver.matrix.membership.rejected', ...)
directly which can throw and bubble up, aborting PDU processing; wrap that emit
call in its own try/catch so any exceptions from listeners are swallowed or
logged (e.g. via this.logger.warn/error) and do not rethrow, ensuring processing
continues; change the block inside the if that calls eventEmitterService.emit to
catch and handle errors locally while preserving the existing payload and
control flow.

---

Nitpick comments:
In `@docs/partial-room-state-for-invites.md`:
- Around line 5-26: The document uses the noun "invite" in prose; change these
prose occurrences to "invitation" while keeping protocol field and event names
unchanged (e.g., preserve `invite_room_state`, `m.room.create`,
`m.room.join_rules` as-is). Update all prose mentions throughout the file
(including the section describing Synapse behavior and any other occurrences) to
use "invitation" for readability, but do not modify code/protocol identifiers or
example event names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 00207b9b-d2bd-4868-a633-327b25a42481

📥 Commits

Reviewing files that changed from the base of the PR and between 42cf7d8 and 50be08a.

📒 Files selected for processing (5)
  • docs/partial-room-state-for-invites.md
  • packages/federation-sdk/src/index.ts
  • packages/federation-sdk/src/services/event.service.ts
  • packages/federation-sdk/src/services/state.service.spec.ts
  • packages/federation-sdk/src/services/state.service.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: ricardogarim
Repo: RocketChat/homeserver PR: 184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.448Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.
📚 Learning: 2025-10-10T22:18:31.655Z
Learnt from: sampaiodiego
Repo: RocketChat/homeserver PR: 224
File: packages/federation-sdk/src/services/event-authorization.service.ts:261-268
Timestamp: 2025-10-10T22:18:31.655Z
Learning: In packages/federation-sdk/src/services/state.service.ts, the method `StateService.getLatestRoomState(roomId: string)` has return type `Promise<State>` and never returns undefined. If the state is not found, it throws an error with message "No state found for room ${roomId}" instead of returning undefined.

Applied to files:

  • packages/federation-sdk/src/services/event.service.ts
  • packages/federation-sdk/src/services/state.service.ts
  • packages/federation-sdk/src/services/state.service.spec.ts
📚 Learning: 2025-09-14T13:34:12.448Z
Learnt from: ricardogarim
Repo: RocketChat/homeserver PR: 184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.448Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • docs/partial-room-state-for-invites.md
🪛 LanguageTool
docs/partial-room-state-for-invites.md

[style] ~5-~5: The noun “invitation” is usually used instead of ‘invite’ in formal writing.
Context: ...rgeting users who have only received an invite but have not yet joined the room. This ...

(AN_INVITE)


[style] ~26-~26: The noun “invitation” is usually used instead of ‘invite’ in formal writing.
Context: ...s where the server has only received an invite. When a server receives an invite, the ...

(AN_INVITE)


[style] ~26-~26: The noun “invitation” is usually used instead of ‘invite’ in formal writing.
Context: ...ed an invite. When a server receives an invite, the unsigned.invite_room_state field...

(AN_INVITE)


[style] ~74-~74: The noun “invitation” is usually used instead of ‘invite’ in formal writing.
Context: ...tries to re-invite the user — builds an invite event whose prev_events reference the...

(AN_INVITE)

🔇 Additional comments (3)
packages/federation-sdk/src/index.ts (1)

127-130: Typed event contract addition looks good.

The new homeserver.matrix.membership.rejected signature is clear and aligns well with the event payload shape used by the emitter flow.

packages/federation-sdk/src/services/state.service.spec.ts (1)

2265-2290: Good coverage for the membership-set expansion scenario.

This test cleanly validates the intended join + invite + ban inclusion while still excluding leave.

packages/federation-sdk/src/services/state.service.ts (1)

700-704: This membership expansion was intentionally added per commit 50be08a.

The change to include invite and ban states in resident memberships is documented in the commit message as intentional: "Updated to match the new behavior where these servers are included to ensure they receive room events." The accompanying test confirms this expectation (lines 2287-2288 assert that banned.com and invited.com are included).

If you believe the spec or security requirements should exclude invited/banned servers from receiving ephemeral events (typing, presence, receipts), that's a valid design concern—but it should be addressed by reconsidering the intentional feature, not as an unintended side effect.

			> Likely an incorrect or invalid review comment.

Comment on lines +170 to +175
if (err instanceof Error && err.name === 'UnknownRoomError' && event.type === 'm.room.member') {
await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
event,
reason: err.message,
});
} else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard the rejection emit path so listener failures don’t abort PDU processing.

If eventEmitterService.emit(...) throws, this catch branch now bubbles and can fail the whole transaction. Validation failures here should remain fail-soft and continue.

💡 Suggested hardening
-						if (err instanceof Error && err.name === 'UnknownRoomError' && event.type === 'm.room.member') {
-							await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
-								event,
-								reason: err.message,
-							});
+						if (err instanceof Error && err.name === 'UnknownRoomError' && event.type === 'm.room.member') {
+							try {
+								await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
+									event,
+									reason: err.message,
+								});
+							} catch (emitErr) {
+								this.logger.error({
+									msg: 'Failed to emit membership rejection event',
+									origin,
+									event,
+									err: emitErr,
+								});
+							}
 						} else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (err instanceof Error && err.name === 'UnknownRoomError' && event.type === 'm.room.member') {
await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
event,
reason: err.message,
});
} else {
if (err instanceof Error && err.name === 'UnknownRoomError' && event.type === 'm.room.member') {
try {
await this.eventEmitterService.emit('homeserver.matrix.membership.rejected', {
event,
reason: err.message,
});
} catch (emitErr) {
this.logger.error({
msg: 'Failed to emit membership rejection event',
origin,
event,
err: emitErr,
});
}
} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/federation-sdk/src/services/event.service.ts` around lines 170 -
175, The UnknownRoomError branch currently calls
this.eventEmitterService.emit('homeserver.matrix.membership.rejected', ...)
directly which can throw and bubble up, aborting PDU processing; wrap that emit
call in its own try/catch so any exceptions from listeners are swallowed or
logged (e.g. via this.logger.warn/error) and do not rethrow, ensuring processing
continues; change the block inside the if that calls eventEmitterService.emit to
catch and handle errors locally while preserving the existing payload and
control flow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this document could actually be two issues right? they don't have value as "documentation"

Content moved to GitHub issues #387 and #388.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ggazzo
Copy link
Copy Markdown
Member Author

ggazzo commented Mar 27, 2026

Split into #389 and #390

@ggazzo ggazzo closed this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants