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

not found

Error message

not found

What it means

UpdateDiscussionThreadReadState#resolve raises GraphQL::ExecutionError "not found" directly (not via rescue) when DiscussionEntry.find succeeds but root_entry.grants_right?(current_user, session, :read) is false. The record exists, but the caller must not learn that, so Canvas reports it as not found.

Solutions

  1. Verify root_entry.grants_right?(current_user, session, :read) before the mutation call.
  2. Confirm the client is authenticated as the intended user (token vs session mismatch).
  3. Check the entry's topic publish state and availability windows for that user's role.
  4. Fix section/enrollment restrictions if the user legitimately needs access.

Example fix

// before
raise GraphQL::ExecutionError, "not found" unless root_entry.grants_right?(current_user, session, :read)
// after
unless root_entry.grants_right?(current_user, session, :read)
  raise GraphQL::ExecutionError, "not found" # ensure client pre-checks read access
end
Defensive patterns

Strategy: type-guard

Validate before calling

query { discussionEntry(id: $id) { permissions { read } } }
// only mutate when data.discussionEntry.permissions.read === true

Type guard

function isReadableEntry(entry) { return !!entry && entry.permissions?.read === true; }

Try / catch

if (!isReadableEntry(entry)) return skip();
try {
  await updateDiscussionThreadReadState(...);
} catch (e) {
  if (e.message === 'not found') handleForbiddenAsNotFound();
}

Prevention

When it happens

Trigger: updateDiscussionThreadReadState with a discussion_entry_id in a topic the user cannot read: unpublished/ delayed topic, unenrolled user, wrong course context, or entry whose root topic is graded and hidden until submission.

Common situations: Observer accounts without read rights in that section; entry ID from a different course after client-side mixup; API key scoped to a token-user lacking enrollment; students replying before topic availability date.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at app/graphql/mutations/update_discussion_thread_read_state.rb:30

# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

class Mutations::UpdateDiscussionThreadReadState < Mutations::BaseMutation
  graphql_name "UpdateDiscussionThreadReadState"

  argument :discussion_entry_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionEntry")
  argument :read, Boolean, required: true

  field :discussion_entry, Types::DiscussionEntryType, null: false
  def resolve(input:)
    root_entry = DiscussionEntry.find(input[:discussion_entry_id])
    raise GraphQL::ExecutionError, "not found" unless root_entry.grants_right?(current_user, session, :read)

    read_state = input[:read] ? "read" : "unread"

    DiscussionEntryParticipant.upsert_for_root_entry_and_descendants(root_entry,
                                                                     current_user,
                                                                     new_state: read_state,
                                                                     forced: true)

    topic = root_entry.discussion_topic
    total_read_count = topic.discussion_entry_participants.read.where(
      discussion_entry_participants: { user_id: current_user.id }
    ).count
    topic.update_or_create_participant(current_user:, new_count: topic.default_unread_count - total_read_count)

    {
      discussion_entry: root_entry
    }
  rescue ActiveRecord::RecordNotFound

View on GitHub (pinned to 1c9f0bb801)