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
- Pass exactly one of ids or sisIds; clear the other key entirely.
- Remember an empty array still counts as provided — a [] lookup will pass this check but return nothing; filter out empty lists client-side.
- 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
- Delete the unused list key entirely; an empty array still counts as provided.
- Reuse a single helper that takes {ids} or {sisIds} and never both.
- Guard against empty arrays client-side to avoid a guaranteed-empty result.
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
- Must specify an id or an assignment_id and user_id or an…
- Must specify exactly one of id or name
- Must specify exactly one of id or sisId
- A maximum of 50 assessees can be provided at once
- A maximum of 50 assessors can be provided at once
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 doView on GitHub (pinned to 1c9f0bb801)