{"record":{"id":"47f1c4c22502605d","repo":"instructure/canvas-lms","slug":"filter-keys-must-be-non-empty-strings","errorCode":null,"errorMessage":"filter keys must be non-empty strings","messagePattern":"filter keys must be non-empty strings","errorType":"validation","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/update_widget_dashboard_config.rb","lineNumber":79,"sourceCode":"\n  # TODO: Remove this entire helper method once platform-ui passes dashboard_type (EGG-2539)\n  # The resolver should directly use input[:dashboard_type] after that.\n  def educator_dashboard?(input)\n    return input[:dashboard_type] == \"educator\" if input[:dashboard_type].present?\n\n    context[:domain_root_account]&.feature_enabled?(:educator_dashboard) &&\n      current_user.educator_dashboard_user?\n  end\n\n  def validate_filters!(widget_id, filters)\n    # Accept Hash or ActionController::Parameters (which GraphQL JSON type may produce)\n    unless filters.is_a?(Hash) || filters.is_a?(ActionController::Parameters)\n      raise GraphQL::ExecutionError, \"filters must be an object\"\n    end\n\n    filters.each_key do |key|\n      unless key.is_a?(String) && !key.empty?\n        raise GraphQL::ExecutionError, \"filter keys must be non-empty strings\"\n      end\n    end\n\n    validate_filter_structure!(widget_id, filters)\n  end\n\n  def validate_filter_structure!(widget_id, filters)\n    if widget_id == ANNOUNCEMENTS_WIDGET_ID\n      validate_announcements_filters!(filters)\n    elsif widget_id == TODO_LIST_WIDGET_ID\n      validate_todo_list_filters!(filters)\n    elsif COURSE_WORK_WIDGET_IDS.include?(widget_id)\n      validate_course_work_filters!(filters)\n    else\n      validate_generic_filters!(filters)\n    end\n  end\n","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/update_widget_dashboard_config.rb#L61-L97","documentation":"validate_filters! iterates filters.each_key and requires every key to be a non-empty String. JSON object keys are normally strings, but keys built dynamically (numbers coerced, empty names, or ActionController::Parameters with symbol keys) trigger this error.","triggerScenarios":"Passing filters with an empty-string key ({\"\": [...]}) or non-string keys (numeric keys when serialized from an array-like object; symbol keys via ActionController::Parameters).","commonSituations":"UI builds filter map with an uninitialized filter name; spreading objects with numeric indexes ({...Object.values(x)}); server-side re-invocation of the mutation with unpermitted params.","solutions":["Ensure every filter key is a non-empty string before sending","Filter out empty keys client-side prior to the mutation","Convert numeric/array-like structures into named-key objects","When building params server-side, stringify keys (e.g. params.to_unsafe_h.deep_stringify_keys)"],"exampleFix":"// before\nconst filters = Object.fromEntries(selected.map((v, i) => [i, v])) // numeric keys\n// after\nconst filters = Object.fromEntries(selected.map(v => [String(v.filterName), v.value]))","handlingStrategy":"validation","validationCode":"const clean = Object.fromEntries(Object.entries(filters).filter(([k, v]) => typeof k === 'string' && k.length > 0))","typeGuard":"const hasValidKeys = (f) => Object.keys(f).every(k => typeof k === 'string' && k.length > 0)","tryCatchPattern":"try { await gql(updateWidgetDashboardConfigMutation, {widgetId, filters}) } catch (e) { if (/filter keys must be non-empty/.test(e.message)) { filters = sanitize(filters); /* retry */ } else throw e }","preventionTips":["Filter out empty/unnamed keys before sending","Avoid spreading array-like objects into filters","Stringify keys when building params dynamically"],"tags":["graphql","validation","keys"],"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"}