instructure/canvas-lms · error · GraphQL::ExecutionError

relative entry not found

Error message

relative entry not found

What it means

DiscussionEntryLoader#perform rescues ActiveRecord::RecordNotFound raised while building the entries scope and re-raises it as the GraphQL::ExecutionError 'relative entry not found'. This happens when a relative-entry filter references a discussion entry (root_entry_id/relative entry id) that cannot be found in the topic's entries — typically the referenced entry was deleted or belongs to a different topic.

Solutions

  1. Verify the relative entry id exists and belongs to the same discussion topic (GET the entry first or refetch the topic's entries).
  2. Re-query without the relative-entry filter to get a fresh cursor, then paginate from there.
  3. Clear stale client-side cursors/entry ids after moderation or topic edits.
  4. Use root_entries pagination instead of relative-entry anchoring when entries may be deleted concurrently.
  5. If you are developing in the loader, consider returning DiscussionEntry.none instead of raising for a missing relative entry to degrade gracefully.

Example fix

// before
rescue ActiveRecord::RecordNotFound
  raise GraphQL::ExecutionError, "relative entry not found"
end
// after
rescue ActiveRecord::RecordNotFound
  fulfill(object, DiscussionEntry.none) # or re-check the entry id before querying
end
Defensive patterns

Strategy: try-catch

Validate before calling

// before paginating with a relative entry
const entry = await fetchDiscussionEntry(entryId)
if (!entry || entry.discussion_topic_id !== topicId || entry.workflow_state === "deleted")
  throw new Error("relative entry no longer exists; refetch topic entries")

Type guard

const isUsableRelativeEntry = (e) => !!e && e.workflow_state !== "deleted" && e.discussion_topic_id === topicId

Try / catch

try {
  const page = await client.request(RELATIVE_ENTRIES_QUERY, { topicId, relativeEntryId })
} catch (e) {
  if (e.message === "relative entry not found") {
    // refetch first page to obtain a fresh cursor
    return client.request(ENTRIES_QUERY, { topicId })
  }
  throw e
}

Prevention

When it happens

Trigger: A discussion entries connection query passing a relative entry id (e.g. filter with relativeEntryId / 'after' cursor anchored to an entry) whose DiscussionEntry does not exist in scope_for(object): deleted entry, wrong topic, or fabricated id.

Common situations: Frontends caching entry ids and paginating after an entry a moderator later deleted; passing an entry id from one discussion into another topic's entriesConnection; clients decoding cursors from older data after entry cleanup.

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/ce520b2ce5aef86a. Report an issue: GitHub.

Appendix: source

Thrown at app/graphql/loaders/discussion_entry_loader.rb:86

                  scope.active.where(UserSearch.like_condition("message"), pattern: UserSearch.like_string_for(@search_term))
                end
      end

      if @relative_entry_id
        relative_entry = scope.find(@relative_entry_id)
        condition = @before_entry ? "<" : ">"
        condition += "=" if @include_entry
        scope = scope.where("created_at #{condition}?", relative_entry.created_at)
      end

      # unread filter is used like search results and need to exclude deleted entries
      scope = scope.active.unread_for_user_before(@current_user, @unread_before) if @filter == "unread"
      scope = scope.where(workflow_state: "deleted") if @filter == "deleted"
      scope = scope.where(user_id: @user_search_id) unless @user_search_id.nil?
      scope = scope.preload(:user, :editor)
      fulfill(object, scope)
    rescue ActiveRecord::RecordNotFound
      raise GraphQL::ExecutionError, "relative entry not found"
    end
  end

  def scope_for(object)
    if object.is_a?(DiscussionTopic)
      object.discussion_entries
    elsif object.is_a?(DiscussionEntry)
      if object.root_entry_id.nil?
        if @user_search_id
          object.flattened_discussion_subentries
        else
          object.root_discussion_replies
        end
      else
        object.discussion_subentries
      end
    end
  end

View on GitHub (pinned to 1c9f0bb801)