instructure/canvas-lms · error · ActiveRecord::RecordNotFound

not found

Error message

not found

What it means

This GraphQL mutation converts a rescued ActiveRecord::RecordNotFound into a GraphQL::ExecutionError with message "not found". In resolve, the topic lookup (DiscussionTopic.find) or the read-permission check raises RecordNotFound when the discussion topic ID does not exist or the current user may not read it. The mutation deliberately flattens both cases into one client-facing error.

Solutions

  1. Verify the discussion_topic_id exists via the discussionTopic query before submitting the entry.
  2. Confirm the current user can read the topic (enrollment active, topic not ungraded-and-hidden, correct context).
  3. Refresh the page/topic list to pick up a new topic id after deletions or migrations.
  4. Check for shard/context mismatch: ids must belong to the same root account shard.

Example fix

// before: blindly using a cached id
createDiscussionEntry({discussionTopicId: cachedTopicId, message})

// after: confirm topic first
const topic = await fetchDiscussionTopic(cachedTopicId)
if (!topic) { redirectToDiscussionList(); return }
await createDiscussionEntry({discussionTopicId: topic.id, message})
Defensive patterns

Strategy: try-catch

Validate before calling

const topic = await client.query({query: TOPIC_QUERY, variables: {id: topicId}})
if (!topic?.data?.legacyNode) throw new Error('topic does not exist')

Try / catch

try {
  await createDiscussionEntry({discussionTopicId, message})
} catch (e) {
  if (/not found/i.test(e.message)) showFlash('Discussion not found or unavailable.')
  else throw e
}

Prevention

When it happens

Trigger: Calling mutation createDiscussionEntry with a discussion_topic_id that does not exist, an already-deleted topic, or a topic the user lacks :read rights on (also parent_entry_id/quoted_entry_id lookups that fail later in resolve).

Common situations: Frontend caching a topic id after the topic was deleted; cross-shard or cross-course id reuse; a student passing another course's topic id; stale Relay global ids after data resets in test environments.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/738871a92fba94f9. Report an issue: GitHub.

Appendix: source

Thrown at app/graphql/mutations/create_discussion_entry.rb:36

# with this program. If not, see <http://www.gnu.org/licenses/>.
#

class Mutations::CreateDiscussionEntry < Mutations::BaseMutation
  graphql_name "CreateDiscussionEntry"

  argument :discussion_topic_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionTopic")
  argument :file_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("Attachment")
  argument :message, String, required: true
  argument :parent_entry_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionEntry")

  argument :is_anonymous_author, Boolean, required: false
  argument :quoted_entry_id, ID, required: false, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionEntry")

  field :discussion_entry, Types::DiscussionEntryType, null: true
  field :my_sub_assignment_submissions, [Types::SubmissionType], null: true
  def resolve(input:)
    topic = DiscussionTopic.find(input[:discussion_topic_id])
    raise ActiveRecord::RecordNotFound unless topic.grants_right?(current_user, session, :read)

    # if the user is writing a threaded reply when the allow threaded replies feature is disabled
    if !topic.threaded? && !input[:parent_entry_id].nil?
      return validation_error(I18n.t("Threaded replies are not allowed in this context"))
    end

    association = topic.discussion_entries
    entry = build_entry(association, input[:message], topic, !!input[:is_anonymous_author])

    if input[:parent_entry_id]
      parent_entry = topic.discussion_entries.find(input[:parent_entry_id])
      entry.parent_entry = parent_entry
    end

    if input[:quoted_entry_id] && DiscussionEntry.find(input[:quoted_entry_id])
      entry.quoted_entry_id = input[:quoted_entry_id]
    end

View on GitHub (pinned to 1c9f0bb801)