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
- Verify the discussion_topic_id exists via the discussionTopic query before submitting the entry.
- Confirm the current user can read the topic (enrollment active, topic not ungraded-and-hidden, correct context).
- Refresh the page/topic list to pick up a new topic id after deletions or migrations.
- 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
- Always resolve topic ids from a live query, never long-lived caches.
- Check topic permissions before rendering the composer.
- Handle deletion events (subscriptions/polling) by invalidating open reply forms.
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)