Skip to content

fix: stop a null user profile killing the server from a notification payload - #6321

Draft
hero101 wants to merge 1 commit into
developfrom
fix/notification-null-profile
Draft

fix: stop a null user profile killing the server from a notification payload#6321
hero101 wants to merge 1 commit into
developfrom
fix/notification-null-profile

Conversation

@hero101

@hero101 hero101 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What

Three notification payload builders dereference user.profile.displayName unguarded. A user row with a null profile makes the builder throw — and because notifyPlatformGlobalRoleChange calls its builder without await and without a catch, that rejection escapes as an unhandled rejection. Under Node's default --unhandled-rejections=throw, the process terminates.

Observed twice during live verification of workspace#027-platform-role-redesign — once via platform role revocation, once via createDiscussion — each time taking the whole server down mid-run.

Why it isn't just a missing relation

Every caller loads the user with relations: { profile: true }. A null profile therefore means the row itself is incomplete, not that someone forgot to request it. Guarding is the correct response; adding the relation would change nothing.

Changes

notification.external.adapter.ts New resolveUserDisplayName() — falls back to the user's name, then their email, so a notification still carries a usable identifier instead of failing to send. Applied at all three sites: buildPlatformUserRemovedNotificationPayload, getUserPayloadOrFail, createUserPayloadFromUser.
platform.role.resolver.mutations.ts notifyPlatformGlobalRoleChange wraps its dispatch in try/catch, logging through Winston. Notifying is best-effort by design; with un-awaited call sites it must never be able to take the process down, whatever the adapter does next.

Both halves matter independently: the guard fixes the trigger seen so far, the try/catch stops any future notifier failure from escalating from "notification not sent" to "server exited".

Provenance

Pre-existing on develop; not introduced by 027. That feature only makes the role-revocation path hot enough to hit it reliably. Filed separately rather than inside the 27-commit role-redesign PR, which is under a security hold and will not ship soon.

Verification

pnpm lint clean (tsc + biome, 3096 files). No test added — reproducing it needs a user row with a null profile, which is a data state rather than something the suite can construct through the API; happy to add one against a seeded fixture if you'd prefer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HZSiMjuPXnj2Un3pd8WKC3

…payload

Three notification payload builders dereference `user.profile.displayName`
without a guard. Every caller loads the user with `relations: { profile: true }`,
so a null profile means the ROW is incomplete — not that the relation was
forgotten — and one such row makes the builder throw.

That alone would be a failed request. It is worse than that:
`notifyPlatformGlobalRoleChange` invokes its builder WITHOUT `await` and
without a catch (both call sites), so the rejection escapes as an unhandled
rejection, and under Node's default `--unhandled-rejections=throw` the process
terminates.

Observed twice during live verification of
workspace#027-platform-role-redesign — once via platform role revocation, once
via `createDiscussion` — each time taking the whole server down mid-run.

- `resolveUserDisplayName()` falls back to the user's name, then their email,
  so a notification still carries a usable identifier instead of failing.
  Applied at all three sites (`buildPlatformUserRemovedNotificationPayload`,
  `getUserPayloadOrFail`, `createUserPayloadFromUser`).
- `notifyPlatformGlobalRoleChange` wraps its dispatch in try/catch and logs
  through Winston. Notifying is best-effort by design; with un-awaited call
  sites it must never be able to take the process down, whatever the adapter
  does next.

Pre-existing on develop and not introduced by 027 — that feature only makes
the role-revocation path hot enough to hit it reliably.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 31, 2026 09:47
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e0d8301-ae3f-4f32-adb5-879dbded396d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actions

Copy link
Copy Markdown

📊 PR Metrics Summary

Title: fix: stop a null user profile killing the server from a notification payload
Total LOC Changed: 58
Files Changed: 2
Proposed Review Type: HUMAN_AUGMENTED_LLM
Rationale:

  • high_risk_keyword
  • LOC<=100
  • files<=10
  • low_risk_keyword

Flags

  • High Risk Keyword
  • Low Risk Keyword
  • Composite High Risk Trigger

Thresholds

{
  "critical_loc": 200,
  "simple_loc": 100,
  "file_count": 10
}

@github-actions

Copy link
Copy Markdown

Schema Diff Summary: No blocking changes

Category Count
breaking 0
prematureRemoval 0
invalidDeprecation 0
deprecated 0
additive 0
info 0

Baseline branch: develop
Current schema MD5: 0d4505323913e76f67c7567f3e3e180a (size 319465)
Previous schema MD5: 0d4505323913e76f67c7567f3e3e180a

Copilot AI left a comment

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.

Pull request overview

Prevents notification payload generation and best-effort notification dispatch from crashing the NestJS server when a User has a null profile (and when notification dispatch fails more generally), addressing an unhandled-rejection process termination scenario.

Changes:

  • Added resolveUserDisplayName() in the external notification adapter and used it at the three previously unguarded user.profile.displayName dereferences.
  • Wrapped platformGlobalRoleChanged dispatch in notifyPlatformGlobalRoleChange() with try/catch and Winston logging to prevent unhandled rejections from terminating the process.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/services/adapters/notification-external-adapter/notification.external.adapter.ts Adds a displayName fallback helper and applies it to three payload builders that previously dereferenced user.profile.displayName unguarded.
src/platform/platform-role/platform.role.resolver.mutations.ts Adds Winston logger injection and wraps notification dispatch in try/catch to prevent unhandled rejection process termination.

Comment on lines +1279 to +1284
private resolveUserDisplayName(user: IUser): string {
return (
user.profile?.displayName ||
`${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() ||
user.email
);
Comment on lines +202 to +218
// Both call sites above invoke this WITHOUT `await`, so anything this
// rejects with becomes an unhandled rejection — and under Node's default
// `--unhandled-rejections=throw` that terminates the process rather than
// failing the request. Observed twice in live verification: a user row
// with a null profile made the payload builder throw, and the server
// exited mid-run. Notifying is best-effort by design; it must never be
// able to take the process down, whatever the adapter does next.
try {
await this.notificationPlatformAdapter.platformGlobalRoleChanged(
notificationInput
);
} catch (error: any) {
this.logger.error(
`Unable to dispatch platform global role change notification (user=${user.id}, role=${role}, type=${type}): ${error?.message}`,
error?.stack,
LogContext.NOTIFICATIONS
);
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.

2 participants