{"record":{"id":"bf59f387f756f843","repo":"instructure/canvas-lms","slug":"invalid-scope-for-hash","errorCode":null,"errorMessage":"invalid scope for hash","messagePattern":"invalid scope for hash","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"app/models/custom_data.rb","lineNumber":72,"sourceCode":"      yield\n      destroyed? || save\n    end\n  end\n\n  def set_data(scope, val)\n    set_hash_data_from_scope(data_json, \"d/#{scope}\", val)\n  end\n\n  def delete_data(scope)\n    delete_hash_data_from_scope(data_json, \"d/#{scope}\")\n  end\n\n  private\n\n  def hash_data_from_scope(hash, scope)\n    keys = scope.split(\"/\")\n    keys.inject(hash) do |h, k|\n      raise ArgumentError, \"invalid scope for hash\" unless h.is_a?(Hash)\n\n      h[k]\n    end\n  end\n\n  def set_hash_data_from_scope(hash, scope, data)\n    keys = scope.split(\"/\")\n    last = keys.pop\n\n    traverse = lambda do |hsh, key_idx|\n      return hsh if key_idx == keys.length\n\n      k = keys[key_idx]\n      h = hsh[k]\n      if h.nil?\n        hsh[k] = {}\n      elsif !h.is_a? Hash\n        raise WriteConflict.new({","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/custom_data.rb#L54-L90","documentation":"CustomData#hash_data_from_scope walks a '/'-delimited scope through a nested hash and raises ArgumentError as soon as an intermediate value is not a Hash. get_data uses it, so reading a scope whose path points into (or through) a non-hash value fails.","triggerScenarios":"Calling get_data(user, namespace, scope) where the scope path traverses a key that was previously set to a scalar (e.g. set_data with scope 'a/b' then get_data with scope 'a/b/c').","commonSituations":"Scope schema drift: a value once stored as a leaf string is now treated as a nested namespace; typos in scope strings; different app versions writing conflicting shapes under one namespace.","solutions":["Correct the scope string so every intermediate segment corresponds to a nested hash.","Delete the conflicting leaf data and re-store it with the correct nested shape via set_data.","Before reading, call get_data on the parent scope and verify it is a Hash."],"exampleFix":"// before\ndata = custom_data.get_data(user, 'profile', 'settings/theme/colors')\n// after\nsettings = custom_data.get_data(user, 'profile', 'settings')\nraise ArgumentError, 'settings is not a hash' unless settings.is_a?(Hash)\ndata = settings.dig('theme', 'colors')","handlingStrategy":"validation","validationCode":"parent = custom_data.get_data(user, namespace, 'settings')\nraise ArgumentError, 'settings is not a hash' unless parent.is_a?(Hash)","typeGuard":"def hash_at_path?(data, scope) = scope.split('/').inject(data) { |h, k| h.is_a?(Hash) ? h[k] : nil }.is_a?(Hash)","tryCatchPattern":"begin\n  data = custom_data.get_data(user, namespace, scope)\nrescue ArgumentError => e\n  Rails.logger.warn(\"bad custom_data scope #{scope}: #{e.message}\")\n  data = nil\nend","preventionTips":["Keep scope schemas stable; never overwrite a hash node with a scalar","Validate scope strings against a documented schema","Read the parent scope and confirm it is a Hash before deep reads"],"tags":["custom-data","scope","argument-error","type-mismatch"],"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"}