{"record":{"id":"cd862196fd0184d0","repo":"toeverything/AFFiNE","slug":"notification-not-found","errorCode":"notification_not_found","errorMessage":"Notification not found.","messagePattern":"Notification not found\\.","errorType":"exception","errorClass":"NotificationNotFound","httpStatus":404,"severity":"warning","filePath":"packages/backend/server/src/core/notification/service.ts","lineNumber":547,"sourceCode":"    this.logger.debug(\n      `Invitation review declined email sent to user ${receiver.id} for workspace ${workspaceId}`\n    );\n  }\n\n  private async ensureWorkspaceContentExists(workspaceId: string) {\n    await this.docReader.getWorkspaceContent(workspaceId);\n  }\n\n  async markAsRead(userId: string, notificationId: string) {\n    try {\n      await this.models.notification.markAsRead(notificationId, userId);\n    } catch (err) {\n      if (\n        err instanceof Prisma.PrismaClientKnownRequestError &&\n        err.code === 'P2025'\n      ) {\n        // https://www.prisma.io/docs/orm/reference/error-reference#p2025\n        throw new NotificationNotFound();\n      }\n      throw err;\n    }\n    await this.publishCountChanged(userId, 'read');\n  }\n\n  async markAllAsRead(userId: string) {\n    await this.models.notification.markAllAsRead(userId);\n    await this.publishCountChanged(userId, 'read-all');\n  }\n\n  /**\n   * Find notifications by user id, order by createdAt desc\n   */\n  async findManyByUserId(userId: string, options?: PaginationInput) {\n    const notifications = await this.models.notification.findManyByUserId(\n      userId,\n      options","sourceCodeStart":529,"sourceCodeEnd":565,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/notification/service.ts#L529-L565","documentation":"NotificationNotFound (code=notification_not_found) thrown by markAsRead when Prisma raises P2025 (record not found) — meaning no notification row matched the (notificationId, userId) pair. The service catches P2025 specifically and rethrows as a typed NotFound; any other Prisma error is rethrown unchanged.","triggerScenarios":"Client calls markAsRead with a notificationId that doesn't exist, belongs to another user, or was already deleted. Race with a delete/clear-all. Id from a stale push payload.","commonSituations":"Stale notification center UI holding ids for notifications cleared on another device. Web push with an old id after the user cleared all. Notification expired/archived between delivery and read.","solutions":["Treat code=notification_not_found as idempotent success on the client (the end state — notification read/gone — is achieved) rather than an error.","Prune local notification lists when a clear-all or bulk action happens to avoid stale ids.","Have markAsRead return void/no-op on not-found instead of throwing if idempotency is desired.","Verify the notificationId shape before submitting to catch obvious client corruption."],"exampleFix":"// before — throws on already-deleted notification\nawait this.models.notification.markAsRead(notificationId, userId);\n\n// after — treat P2025 as success (already in desired state)\nasync markAsRead(userId, notificationId) {\n  try {\n    await this.models.notification.markAsRead(notificationId, userId);\n  } catch (err) {\n    if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') {\n      return; // notification already gone — nothing to mark\n    }\n    throw err;\n  }\n  await this.publishCountChanged(userId, 'read');\n}","handlingStrategy":"try-catch","validationCode":"function assertNotificationId(id: string) {\n  if (!/^[0-9a-f-]{16,}$/i.test(id)) throw new UserError('Invalid notification id');\n}\n// Best prevention: treat not-found as idempotent success on the client.","typeGuard":"function isNotificationNotFound(e: unknown): boolean {\n  return e instanceof Error && (e as any).code === 'notification_not_found';\n}","tryCatchPattern":"try {\n  await service.markAsRead(me.id, notificationId);\n} catch (e) {\n  if (isNotificationNotFound(e)) {\n    // already gone — desired state achieved\n    return;\n  }\n  throw e;\n}","preventionTips":["Treat notification_not_found as idempotent success, not an error to show.","Prune local notification ids after clear-all / bulk actions.","Validate notification id shape before submitting.","Consider making markAsRead no-op on P2025 server-side."],"tags":["notification","not-found","prisma","idempotency","validation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}