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

# not found

Error message

#{e.model} not found

What it means

This GraphQL mutation rescues ActiveRecord::RecordNotFound raised while loading the assignment, submission, or other records in SaveRubricAssessment, and re-raises it as a GraphQL::ExecutionError with the message "<ModelName> not found". The library converts Rails record lookup failures into a GraphQL top-level error so the client receives a structured error instead of an unhandled exception. `e.model` is the class name of the record that could not be found (e.g. Assignment, Submission), so the message tells you which lookup failed.

Solutions

  1. Verify the id(s) sent in the mutation input exist and belong to the current course/shard; re-query the assignment via GraphQL to confirm.
  2. Check the message's model name to see which lookup failed (Assignment vs Submission vs ProvisionalGrade) and correct that specific id.
  3. If the record was deleted, re-select the target or recreate it before saving the rubric assessment.
  4. If you must tolerate missing records, pre-fetch with a scoped `where(id:).first` check in the caller before invoking the mutation.

Example fix

// before
mutation {
  saveRubricAssessment(input: { submissionId: "12345", ... }) { ... }
}
// after
// verify id first
query { legacyNode(__typename: "Submission", id: "12345") { ... } }
// then call the mutation with a confirmed existing submission id
Defensive patterns

Strategy: try-catch

Validate before calling

const exists = await client.query({ query: LEGACY_NODE_QUERY, variables: { id: submissionId } });
if (!exists?.data?.legacyNode) throw new Error(`Submission ${submissionId} not found`);

Type guard

function isRecordAvailable(node) { return node != null && node._id != null; }

Try / catch

try {
  await client.mutate({ mutation: SAVE_RUBRIC_ASSESSMENT, variables });
} catch (e) {
  if (e.graphQLErrors?.some(g => /not found$/.test(g.message))) {
    // refresh ids / notify user the record no longer exists
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling the saveRubricAssessment mutation with an assignment_id, submission_id, or provisional_grade_id that does not exist (or is not visible to the current user) causes any `find` inside resolve to raise ActiveRecord::RecordNotFound, which is re-raised here as "Assignment not found" / "Submission not found" etc.

Common situations: A stale client cache referencing a deleted assignment or submission; passing a peer-review/sub-assignment id instead of the parent assignment id; wrong shard/context (record exists on another root account); typos in ids copied from URLs; the record was soft-deleted so find (not find by active scope) misses it.

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

Appendix: source

Thrown at app/graphql/mutations/save_rubric_assessment.rb:87

        rubric_assessment = association.assess(
          assessor: current_user,
          user:,
          artifact: asset,
          assessment: assessment_details,
          graded_anonymously: input[:graded_anonymously]
        )

        submission.reload
        return { submission:, rubric_assessment:, rubric_association: association }
      end
    rescue Assignment::MaxGradersReachedError => e
      raise GraphQL::ExecutionError, e.message
    rescue Assignment::GradeError
      raise GraphQL::ExecutionError, "Assignment Grade Error"
    end
  rescue ActiveRecord::RecordNotFound => e
    raise GraphQL::ExecutionError, "#{e.model} not found"
  end

  def ensure_adjudication_possible(provisional:, association_object:, grader:, &)
    # Non-assignment association objects crash if they're passed into this
    # controller, since find_asset_for_assessment only exists on assignments.
    # The check here thus serves only to make sure the crash doesn't happen on
    # the call below.
    return yield unless association_object.is_a?(Assignment)

    association_object.ensure_grader_can_adjudicate(
      grader:,
      provisional:,
      occupy_slot: true,
      &
    )
  end
end

View on GitHub (pinned to 1c9f0bb801)