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
- Verify root_entry.grants_right?(current_user, session, :read) before the mutation call.
- Confirm the client is authenticated as the intended user (token vs session mismatch).
- Check the entry's topic publish state and availability windows for that user's role.
- 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
- Gate thread-level mutations on the root topic's read permission
- Filter entry lists by user-visible topics server-side before caching
- Handle 'not found' as possible authorization failure, not just missing row
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
- insufficient permission
- Insufficient Permissions
- not found
- ActiveRecord::RecordNotFound
- Cannot set rubric self assessment for discussion assignments
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::RecordNotFoundView on GitHub (pinned to 1c9f0bb801)