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

Must specify exactly one of ids or sisIds

Error message

Must specify exactly one of ids or sisIds

What it means

The `courses` plural resolver requires exactly one of `ids` (graphql/legacy course IDs) or `sis_ids`. Supplying both or neither is rejected with this GraphQL::ExecutionError before the (separate) 100-item limit check.

Solutions

  1. Pass exactly one of ids or sisIds; clear the other key entirely.
  2. Remember an empty array still counts as provided — a [] lookup will pass this check but return nothing; filter out empty lists client-side.
  3. Split larger requests: this error is about argument shape; the 100-item cap is a distinct error on the same field.

Example fix

// before
variables: {"ids":[], "sisIds":["C-1","C-2"]}
// after
variables: {"sisIds":["C-1","C-2"]}
Defensive patterns

Strategy: validation

Validate before calling

const provided = [vars.ids, vars.sisIds].filter(a => Array.isArray(a));
if (provided.length !== 1) throw new Error('courses requires exactly one of ids or sisIds');
if (provided[0].length === 0) throw new Error('empty id list');
if (provided[0].length > 100) throw new Error('split into batches of <=100');

Type guard

const coursesArgsValid = (vars) => {
  const lists = ['ids','sisIds'].filter(k => Array.isArray(vars[k]));
  return lists.length === 1 && vars[lists[0]].length > 0 && vars[lists[0]].length <= 100;
};

Try / catch

try { return await gql(COURSES_QUERY, vars); } catch (e) { if (/exactly one of ids or sisIds/.test(e.message)) { deleteEmptyList(vars); return retry(vars); } throw e; }

Prevention

When it happens

Trigger: query { courses(ids: [...], sisIds: [...]) } (both lists) or query { courses } with no list (neither). Empty arrays serialize as present, so [] counts as a supplied value for the either/neither check.

Common situations: A variables object where one list was set and the other left as an empty array from a previous search; switching between ID and SIS lookup modes while retaining the prior key; persisted queries that always declare both arguments.

Related errors


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

Appendix: source

Thrown at app/graphql/types/query_type.rb:257

                                  .order(
                                    Arel.sql("enrollments.course_id"),
                                    Arel.sql("enrollments.user_id"),
                                    Enrollment.state_by_date_rank_sql,
                                    Arel.sql("enrollments.id")
                                  )

      InstructorQuery.new(deduplicated_ids_subquery)
    end

    field :courses,
          [Types::CourseType],
          "Courses by IDs that are viewable by the current user",
          null: true do
      argument :ids, [ID], "graphql or legacy course IDs", required: false, prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func("Course")
      argument :sis_ids, [String], "ids from the original SIS system", required: false
    end
    def courses(ids: nil, sis_ids: nil)
      raise GraphQL::ExecutionError, "Must specify exactly one of ids or sisIds" if (ids && sis_ids) || !(ids || sis_ids)

      course_ids = ids || sis_ids
      raise GraphQL::ExecutionError, "Cannot request more than 100 courses at once" if course_ids&.length.to_i > 100

      courses = if ids
                  current_user&.accessible_courses_by_ids(ids, preload_courses: true)
                elsif sis_ids
                  current_user&.accessible_courses_by_sis_ids(sis_ids, preload_courses: true)
                end

      courses&.index_by(&:id)
             &.values
             &.sort_by! do |course|
               Canvas::ICU.collation_key(course.nickname_for(current_user))
             end
    end

    field :module_item, Types::ModuleItemType, null: true do

View on GitHub (pinned to 1c9f0bb801)