instructure/canvas-lms · error · RuntimeError

Only a hash value is accepted for backup_submission_data…

Error message

Only a hash value is accepted for backup_submission_data calls

What it means

QuizSubmission#backup_submission_data persists a student's in-progress answer snapshot, and only accepts a Hash or ActionController::Parameters for params. Canvas raises this RuntimeError to reject malformed payloads early, since it later deep-merges params into submission_data.

Solutions

  1. Ensure the client sends submission backup data as a JSON object (hash), not an array or string
  2. In the controller, verify params[:quiz][:submission_data] (or equivalent) is a Hash before calling backup_submission_data
  3. If receiving serialized strings, parse them with JSON.parse or symbolize/permit into ActionController::Parameters first

Example fix

// before
submission.backup_submission_data(request.body.read)
// after
params_hash = JSON.parse(request.body.read)
raise ArgumentError unless params_hash.is_a?(Hash)
submission.backup_submission_data(params_hash)
Defensive patterns

Strategy: type-guard

Validate before calling

raise ArgumentError, "params must be a Hash" unless params.is_a?(Hash) || params.is_a?(ActionController::Parameters)

Type guard

def hash_params?(p) = p.is_a?(Hash) || p.is_a?(ActionController::Parameters)

Try / catch

begin
  submission.backup_submission_data(params)
rescue RuntimeError => e
  raise unless e.message.include?("hash value")
  Rails.logger.warn("non-hash backup payload dropped")
end

Prevention

When it happens

Trigger: POSTing backup/answer data to the quiz submission endpoint where params (or a nested attribute like [:quiz][:submission_data]) is a string, array, nil, or other non-hash value.

Common situations: Custom API clients posting JSON bodies in unexpected shapes; frontend sending answers as an array or serialized string; proxies stripping/reshaping the request body; nil params when required fields are omitted.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at app/models/quizzes/quiz_submission.rb:338

    extend ActionView::Helpers::DateHelper

    started_at && finished_at && time_ago_in_words(Time.zone.now - (finished_at - started_at))
  end

  def finished_at_fallback
    [end_at, Time.zone.now].compact.min
  end

  def points_possible_at_submission_time
    questions.filter_map { |q| q[:points_possible].to_f }.sum || 0
  end

  def questions
    Utf8Cleaner.recursively_strip_invalid_utf8!(quiz_data, force_utf8: true) || []
  end

  def backup_submission_data(params)
    raise "Only a hash value is accepted for backup_submission_data calls" unless params.is_a?(Hash) || params.is_a?(ActionController::Parameters)

    params = sanitize_params(params)

    new_params = if !graded? && submission_data[:attempt] == attempt
                   submission_data.deep_merge(params)
                 else
                   params
                 end

    new_params[:attempt] = attempt

    # take a snapshot every 5 other saves:
    new_params[:cnt] ||= 0
    new_params[:cnt] = (new_params[:cnt].to_i + 1) % 5
    snapshot!(params) if new_params[:cnt] == 1

    self.class.where(id: self)
        .where("workflow_state NOT IN ('complete', 'pending_review')")

View on GitHub (pinned to 1c9f0bb801)