instructure/canvas-lms · error · Checkpoints::StudentIdsRequiredError

student_ids is required, but was not provided

Error message

student_ids is required, but was not provided

What it means

Checkpoints::AdhocOverrideUpdaterService updates an existing adhoc override's student list. `call` resolves desired_student_ids from the @override hash, falling back to the IDs currently on override.set; if both sources yield a blank value it raises StudentIdsRequiredError. The update cannot proceed without a target set of students.

Solutions

  1. Supply a non-empty student_ids array in the @override hash to the service.
  2. Verify the existing override actually has AssignmentOverrideStudents rows; if the goal is removal, delete the override instead of updating with an empty list.
  3. Seed valid student IDs (ids present in checkpoint.course.all_students) since invalid ids are silently filtered out and may leave the list blank.

Example fix

// before
AdhocOverrideUpdaterService.call(..., override: {id: ov.id, student_ids: []})
// after
AdhocOverrideUpdaterService.call(..., override: {id: ov.id, student_ids: valid_student_ids})
Defensive patterns

Strategy: validation

Validate before calling

ids = payload[:student_ids].presence || override.assignment_override_students.pluck(:user_id)
raise ArgumentError, 'no students to update' if ids.blank?

Type guard

def updatable_student_ids?(override, payload)
  Array(payload[:student_ids]).any? || override.set.any?
end

Try / catch

begin
  Checkpoints::AdhocOverrideUpdaterService.call(...)
rescue Checkpoints::StudentIdsRequiredError
  # override has no students anywhere; delete it instead
  Checkpoints::OverrideDeleterService.call(...)
end

Prevention

When it happens

Trigger: Calling AdhocOverrideUpdaterService#call where the @override hash has blank/absent :student_ids AND the existing AssignmentOverride has an empty .set (no override students), so the fallback also yields [].

Common situations: Updating an override that was created empty/corrupted (no override students); sending an update payload with student_ids: [] intending a delete but using the updater instead of a deleter; renamed param keys in API layer.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at app/services/checkpoints/adhoc_override_updater_service.rb:29

#
# 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 Checkpoints::AdhocOverrideUpdaterService < Checkpoints::AdhocOverrideCommonService
  def call
    override = @checkpoint.assignment_overrides.find_by(id: @override[:id], set_type: AssignmentOverride::SET_TYPE_ADHOC)
    raise Checkpoints::OverrideNotFoundError unless override

    # Fetch the student_ids from @override or fall back to getting them from override.set
    desired_student_ids = @override.fetch(:student_ids, nil)
    # If desired_student_ids is not provided, use a map of ids from override.set
    desired_student_ids ||= override.set.map(&:id)
    raise Checkpoints::StudentIdsRequiredError, "student_ids is required, but was not provided" if desired_student_ids.blank?

    valid_student_ids = @checkpoint.course.all_students.where(id: desired_student_ids).pluck(:id).uniq
    existing_student_ids = override.assignment_override_students.pluck(:user_id)
    student_ids_to_delete = existing_student_ids - valid_student_ids

    parent_override = override.parent_override

    destroy_students(override:, student_ids: student_ids_to_delete)
    destroy_students(override: parent_override, student_ids: student_ids_to_delete)

    update_override(override:, student_ids: valid_student_ids)
    update_override(override: parent_override, student_ids: valid_student_ids, shell_override: true)

    override
  end

  def destroy_students(override:, student_ids:)
    return if student_ids.blank?

View on GitHub (pinned to 1c9f0bb801)