{"record":{"id":"ae6b4b61d74a3379","repo":"instructure/canvas-lms","slug":"only-a-hash-value-is-accepted-for-backup-submission-data","errorCode":null,"errorMessage":"Only a hash value is accepted for backup_submission_data calls","messagePattern":"Only a hash value is accepted for backup_submission_data calls","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"app/models/quizzes/quiz_submission.rb","lineNumber":338,"sourceCode":"    extend ActionView::Helpers::DateHelper\n\n    started_at && finished_at && time_ago_in_words(Time.zone.now - (finished_at - started_at))\n  end\n\n  def finished_at_fallback\n    [end_at, Time.zone.now].compact.min\n  end\n\n  def points_possible_at_submission_time\n    questions.filter_map { |q| q[:points_possible].to_f }.sum || 0\n  end\n\n  def questions\n    Utf8Cleaner.recursively_strip_invalid_utf8!(quiz_data, force_utf8: true) || []\n  end\n\n  def backup_submission_data(params)\n    raise \"Only a hash value is accepted for backup_submission_data calls\" unless params.is_a?(Hash) || params.is_a?(ActionController::Parameters)\n\n    params = sanitize_params(params)\n\n    new_params = if !graded? && submission_data[:attempt] == attempt\n                   submission_data.deep_merge(params)\n                 else\n                   params\n                 end\n\n    new_params[:attempt] = attempt\n\n    # take a snapshot every 5 other saves:\n    new_params[:cnt] ||= 0\n    new_params[:cnt] = (new_params[:cnt].to_i + 1) % 5\n    snapshot!(params) if new_params[:cnt] == 1\n\n    self.class.where(id: self)\n        .where(\"workflow_state NOT IN ('complete', 'pending_review')\")","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/quizzes/quiz_submission.rb#L320-L356","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the client sends submission backup data as a JSON object (hash), not an array or string","In the controller, verify params[:quiz][:submission_data] (or equivalent) is a Hash before calling backup_submission_data","If receiving serialized strings, parse them with JSON.parse or symbolize/permit into ActionController::Parameters first"],"exampleFix":"// before\nsubmission.backup_submission_data(request.body.read)\n// after\nparams_hash = JSON.parse(request.body.read)\nraise ArgumentError unless params_hash.is_a?(Hash)\nsubmission.backup_submission_data(params_hash)","handlingStrategy":"type-guard","validationCode":"raise ArgumentError, \"params must be a Hash\" unless params.is_a?(Hash) || params.is_a?(ActionController::Parameters)","typeGuard":"def hash_params?(p) = p.is_a?(Hash) || p.is_a?(ActionController::Parameters)","tryCatchPattern":"begin\n  submission.backup_submission_data(params)\nrescue RuntimeError => e\n  raise unless e.message.include?(\"hash value\")\n  Rails.logger.warn(\"non-hash backup payload dropped\")\nend","preventionTips":["Send answer backup data as JSON objects, never arrays or strings","Permit/sanitize controller params before passing them on","Validate payload shape client-side before POSTing"],"tags":["ruby","rails","quiz-submission","invalid-argument"],"backgroundTag":"invalid-argument-value","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"}