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
- 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
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
- 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
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
- Can't update submission scores unless it's completed
- Invalid consumer #
- Quizzes::QuizSubmission.update_scores called on a quiz that…
- assessor and assessee required
- association required
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)