{"record":{"id":"dfd869c7aa3829ab","repo":"instructure/canvas-lms","slug":"must-specify-exactly-one-of-ids-or-sisids","errorCode":null,"errorMessage":"Must specify exactly one of ids or sisIds","messagePattern":"Must specify exactly one of ids or sisIds","errorType":"exception","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/types/query_type.rb","lineNumber":257,"sourceCode":"                                  .order(\n                                    Arel.sql(\"enrollments.course_id\"),\n                                    Arel.sql(\"enrollments.user_id\"),\n                                    Enrollment.state_by_date_rank_sql,\n                                    Arel.sql(\"enrollments.id\")\n                                  )\n\n      InstructorQuery.new(deduplicated_ids_subquery)\n    end\n\n    field :courses,\n          [Types::CourseType],\n          \"Courses by IDs that are viewable by the current user\",\n          null: true do\n      argument :ids, [ID], \"graphql or legacy course IDs\", required: false, prepare: GraphQLHelpers.relay_or_legacy_ids_prepare_func(\"Course\")\n      argument :sis_ids, [String], \"ids from the original SIS system\", required: false\n    end\n    def courses(ids: nil, sis_ids: nil)\n      raise GraphQL::ExecutionError, \"Must specify exactly one of ids or sisIds\" if (ids && sis_ids) || !(ids || sis_ids)\n\n      course_ids = ids || sis_ids\n      raise GraphQL::ExecutionError, \"Cannot request more than 100 courses at once\" if course_ids&.length.to_i > 100\n\n      courses = if ids\n                  current_user&.accessible_courses_by_ids(ids, preload_courses: true)\n                elsif sis_ids\n                  current_user&.accessible_courses_by_sis_ids(sis_ids, preload_courses: true)\n                end\n\n      courses&.index_by(&:id)\n             &.values\n             &.sort_by! do |course|\n               Canvas::ICU.collation_key(course.nickname_for(current_user))\n             end\n    end\n\n    field :module_item, Types::ModuleItemType, null: true do","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/types/query_type.rb#L239-L275","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvariables: {\"ids\":[], \"sisIds\":[\"C-1\",\"C-2\"]}\n// after\nvariables: {\"sisIds\":[\"C-1\",\"C-2\"]}","handlingStrategy":"validation","validationCode":"const provided = [vars.ids, vars.sisIds].filter(a => Array.isArray(a));\nif (provided.length !== 1) throw new Error('courses requires exactly one of ids or sisIds');\nif (provided[0].length === 0) throw new Error('empty id list');\nif (provided[0].length > 100) throw new Error('split into batches of <=100');","typeGuard":"const coursesArgsValid = (vars) => {\n  const lists = ['ids','sisIds'].filter(k => Array.isArray(vars[k]));\n  return lists.length === 1 && vars[lists[0]].length > 0 && vars[lists[0]].length <= 100;\n};","tryCatchPattern":"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; }","preventionTips":["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."],"tags":["graphql","validation","arguments"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}