{"record":{"id":"7d15463c1b8884eb","repo":"instructure/canvas-lms","slug":"message-settings-must-be-an-array-if-present","errorCode":null,"errorMessage":"message_settings must be an Array if present","messagePattern":"message_settings must be an Array if present","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"app/models/context_external_tool.rb","lineNumber":1446,"sourceCode":"  def asset_processor_eula_url\n    Rails.application.routes.url_helpers.update_tool_eula_url(\n      context_external_tool_id: id,\n      host: context.root_account.environment_specific_domain\n    ).delete_suffix(\"/deployment\")\n  end\n\n  def message_settings\n    settings[:message_settings]\n  end\n\n  def message_settings=(value)\n    if value.is_a?(Array)\n      value = value.map(&:with_indifferent_access)\n      value.each do |setting|\n        setting[\"enabled\"] = Canvas::Plugin.value_to_boolean(setting[\"enabled\"]) if setting.is_a?(Hash) && setting.key?(\"enabled\")\n      end\n    elsif value.present?\n      raise ArgumentError, \"message_settings must be an Array if present\"\n    end\n    settings[:message_settings] = value\n  end\n\n  def message_settings_for(message_type)\n    ms = (message_settings || []).select { |ms| ms[\"type\"] == message_type.to_s }\n    ms.is_a?(Array) ? ms.map(&:with_indifferent_access) : ms\n  end\n\n  def eula_settings\n    message_settings_for(LtiAdvantage::Messages::EulaRequest::MESSAGE_TYPE).first || {}\n  end\n\n  def eula_enabled?\n    !!eula_settings&.dig(\"enabled\")\n  end\n\n  def eula_launch_url","sourceCodeStart":1428,"sourceCodeEnd":1464,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/context_external_tool.rb#L1428-L1464","documentation":"ContextExternalTool's message_settings= setter raises ArgumentError when the assigned value is present but not an Array. Only arrays of settings hashes (with optional 'enabled' coerced to boolean) are accepted; any other present value is rejected before being stored in settings.","triggerScenarios":"Assigning tool.message_settings = {type: 'submission', enabled: true} (a Hash, not Array) or a String; passing a JSON object instead of an array when creating/updating an external tool via API.","commonSituations":"LTI tool configuration JSON where message_settings was authored as a single object; YAML/XML config converters that don't wrap the setting in an array; hand-written tool provisioning scripts.","solutions":["Wrap the settings object in an array: [{type: 'submission_type', enabled: true}].","If message_settings is genuinely absent, pass nil or [] rather than a truthy non-array.","Coerce single-object payloads at the API boundary before assignment."],"exampleFix":"// before\ntool.message_settings = { type: 'submission_type', enabled: true }\n// after\ntool.message_settings = [{ type: 'submission_type', enabled: true }]","handlingStrategy":"type-guard","validationCode":"ms = payload[:message_settings]\nraise ArgumentError, 'message_settings must be an Array' if ms.present? && !ms.is_a?(Array)","typeGuard":"def valid_message_settings?(v) = v.nil? || v.is_a?(Array)","tryCatchPattern":"begin\n  tool.message_settings = payload[:message_settings]\nrescue ArgumentError => e\n  render json: { error: e.message }, status: :bad_request\nend","preventionTips":["Always send message_settings as a JSON array of objects","Normalize single-object configs into arrays at ingestion","Validate tool config payloads against the API schema before assignment"],"tags":["lti","external-tools","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"}