instructure/canvas-lms · error

Invalid Attachment

Error message

Invalid Attachment

What it means

During assignment submission (student_annotation type), Canvas raises 'Invalid Attachment' when annotatable_attachment_id is blank, or when the attachment id submitted does not match the assignment's configured annotatable_attachment_id. This guards against stale tabs submitting annotation work against a different file.

Solutions

  1. Refresh the assignment page and resubmit so the current annotatable_attachment_id is used
  2. Verify the API call includes a non-blank annotatable_attachment_id matching the assignment's annotatable_attachment_id
  3. Re-publish/re-select the annotated document consistently, then have students reload before submitting

Example fix

# before
submit({ submission_type: 'student_annotation' })
# after
submit({ submission_type: 'student_annotation', annotatable_attachment_id: assignment.annotatable_attachment_id })
Defensive patterns

Strategy: validation

Validate before calling

const attachmentId = opts.annotatable_attachment_id;
if (attachmentId == null || Number(attachmentId) === 0) {
  throw new Error('annotatable_attachment_id is required for student_annotation submissions');
}
if (Number(attachmentId) !== Number(assignment.annotatable_attachment_id)) {
  throw new Error('Stale tab: reload the page and resubmit');
}

Type guard

function hasValidAnnotatableAttachment(opts, assignment) {
  const id = Number(opts?.annotatable_attachment_id);
  return Number.isInteger(id) && id > 0 && id === Number(assignment?.annotatable_attachment_id);
}

Try / catch

try {
  await submitAnnotation(submission);
} catch (e) {
  if (e.message === 'Invalid Attachment') {
    promptReload('The annotated document changed. Please refresh and try again.');
  }
}

Prevention

When it happens

Trigger: Submitting an ePortfolio/annotation attempt with no annotatable_attachment_id; a student with a stale browser tab submits after the teacher changed the assignment's annotated file so the ids differ.

Common situations: Teacher swaps the annotated document after students began work; student opens submit page, leaves it open, assignment config changes, then submits; direct API calls omitting the attachment id.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at app/models/abstract_assignment.rb:2752

  ].freeze
  ALLOWABLE_SUBMIT_HOMEWORK_OPTS = (SUBMIT_HOMEWORK_ATTRS +
                                    %w[comment group_comment attachments require_submission_type_is_valid resource_link_lookup_uuid student_id]).to_set

  def submit_homework(original_student, opts = {})
    raise "Student Required" unless original_student

    eula_timestamp = opts[:eula_agreement_timestamp]
    webhook_info = assignment_configuration_tool_lookups.take&.webhook_info
    should_add_proxy = false

    if opts[:proxied_student]
      current_user = original_student
      original_student = opts[:proxied_student]
      should_add_proxy = true
    end

    if opts[:submission_type] == "student_annotation"
      raise "Invalid Attachment" if opts[:annotatable_attachment_id].blank?
      raise "Invalid submission type" unless annotated_document?
      # Prevent the case where a user clicks Submit on a stale tab, expecting
      # to submit one set of work, only for another set to be submitted
      # instead.
      raise "Invalid Attachment" if opts[:annotatable_attachment_id].to_i != annotatable_attachment_id
    end

    # Only allow a few fields to be submitted.  Cannot submit the grade of a
    # homework assignment, for instance.
    opts.each_key do |k|
      opts.delete(k) unless ALLOWABLE_SUBMIT_HOMEWORK_OPTS.include?(k.to_s)
    end

    comment = opts.delete(:comment)
    group_comment = opts.delete(:group_comment)
    group, students = group_students(original_student)
    homeworks = []
    primary_homework = nil

View on GitHub (pinned to 1c9f0bb801)