{"record":{"id":"e67b05262c6fc7da","repo":"instructure/canvas-lms","slug":"activerecord-recordnotfound-restore-deleted-discussion-entry","errorCode":null,"errorMessage":"ActiveRecord::RecordNotFound","messagePattern":"ActiveRecord::RecordNotFound","errorType":"exception","errorClass":"ActiveRecord::RecordNotFound","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/restore_deleted_discussion_entry.rb","lineNumber":31,"sourceCode":"# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR\n# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more\n# details.\n#\n# You should have received a copy of the GNU Affero General Public License along\n# with this program. If not, see <http://www.gnu.org/licenses/>.\n#\n\nclass Mutations::RestoreDeletedDiscussionEntry < Mutations::BaseMutation\n  argument :discussion_entry_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func(\"DiscussionEntry\")\n\n  field :discussion_entry, Types::DiscussionEntryType, null: true\n\n  def resolve(input:)\n    entry = DiscussionEntry.find(input[:discussion_entry_id])\n\n    return validation_error(I18n.t(\"Insufficient Permissions\")) unless entry.context.feature_enabled?(:restore_discussion_entry)\n\n    raise ActiveRecord::RecordNotFound unless entry.grants_right?(current_user, session, :read)\n    return validation_error(I18n.t(\"Insufficient Permissions\")) unless entry.grants_right?(current_user, session, :update)\n\n    if entry.deleted?\n      entry.saving_user = current_user\n      entry.restore\n      { discussion_entry: entry }\n    else\n      validation_error(I18n.t(\"Discussion entry is not deleted\"))\n    end\n  end\nend\n","sourceCodeStart":13,"sourceCodeEnd":43,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/restore_deleted_discussion_entry.rb#L13-L43","documentation":"restore_deleted_discussion_entry raises a raw ActiveRecord::RecordNotFound from DiscussionEntry.find(input[:discussion_entry_id]) (or the explicit raise unless the user can :read the entry). Rails' find raises when the id does not exist, including soft-deleted/shard-mismatched rows; the explicit raise hides deleted entries from users without read rights.","triggerScenarios":"discussion_entry_id references a non-existent entry, an entry on a different shard, or an entry the current_user lacks :read rights on (e.g. not enrolled in the course).","commonSituations":"Clients restoring from a stale notifications/email link after the entry was hard-deleted; student attempting to restore another user's entry in a course they can no longer access.","solutions":["Confirm the entry id exists: DiscussionEntry.where(id: id).exists?","Verify current_user has :read and :update rights on the entry's context.","Look up the entry through its discussion_topic within the accessible course to get a clearer error."],"exampleFix":"// before\nentry = DiscussionEntry.find(input[:discussion_entry_id])\n// after\nentry = DiscussionEntry.where(id: input[:discussion_entry_id]).first\nreturn validation_error('Entry not found or inaccessible') unless entry&.grants_right?(current_user, session, :read)","handlingStrategy":"try-catch","validationCode":"// verify the entry is restorable and user has rights\nconst entry = await entryQuery(discussionEntryId)\nif (!entry) return\nif (!entry.viewerCanUpdate) showError('Insufficient Permissions')","typeGuard":"function isEntryAccessible(entry, user) { return !!entry && !entry.deleted && entry.permissions.read === true && entry.permissions.update === true }","tryCatchPattern":"try { await restoreDeletedDiscussionEntry({ id }) } catch (e) { if (e.message.includes('RecordNotFound')) showError('Entry no longer exists') else throw e }","preventionTips":["Hide restore actions for entries the user cannot read/update","Check the restore_discussion_entry feature flag is on for the context","Use GraphQL node queries (returns null) instead of ids that can 404"],"tags":["graphql","record-not-found","permissions"],"backgroundTag":"record-not-found","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}