instructure/canvas-lms · error · GraphQL::ExecutionError

Must specify exactly one of id or sisId

Error message

Must specify exactly one of id or sisId

What it means

Canvas's GraphQL `account` resolver accepts either a GraphQL/legacy `id` or a SIS `sis_id`, but not both and not neither. It raises this GraphQL::ExecutionError when the argument pair is ambiguous or missing, so the lookup target is unambiguous.

Solutions

  1. Pass exactly one of `id` or `sisId`; remove the other argument from the query/variables.
  2. If both values exist, decide which lookup you need (SIS IDs require SIS import context) and drop the other.
  3. Check variable serialization so unset variables are omitted rather than sent as empty strings/nulls.

Example fix

// before
query { account(id: "1", sisId: "acc-100") { name } }
// after
query { account(sisId: "acc-100") { name } }
Defensive patterns

Strategy: validation

Validate before calling

const argCount = [vars.id, vars.sisId].filter(v => v != null && v !== '').length;
if (argCount !== 1) throw new Error('account requires exactly one of id or sisId');

Type guard

const hasExactlyOne = (vars) => ['id','sisId'].filter(k => vars[k] != null && vars[k] !== '').length === 1;

Try / catch

try { await gql(ACCOUNT_QUERY, vars); } catch (e) { if (e.message.includes('Must specify exactly one of id or sisId')) { vars = pickSingleIdentifier(vars, ['id','sisId']); retry } else throw e; }

Prevention

When it happens

Trigger: Calling query { account(...) } with both id and sisId supplied, or with neither supplied (or both null). Any other argument combination (exactly one) is accepted.

Common situations: Clients templating the query pass both fields as variables with one left as an empty value that still serializes; frontend code copies a query snippet and leaves a stale id variable when switching to sisId lookup; omitting arguments entirely when testing in GraphiQL.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/a1504b3cf1053d8c. Report an issue: GitHub.

Appendix: source

Thrown at app/graphql/types/query_type.rb:45

    field :legacy_node, GraphQL::Types::Relay::Node, null: true do
      description "Fetches an object given its type and legacy ID"
      argument :_id, ID, required: true
      argument :type, LegacyNodeType, required: true
    end
    def legacy_node(type:, _id:) # rubocop:disable Lint/UnderscorePrefixedVariableName -- named for DSL reasons
      GraphQLNodeLoader.load(type, _id, context)
    end

    field :account, Types::AccountType, null: true do
      argument :id,
               ID,
               "a graphql or legacy id",
               required: false,
               prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("Account")
      argument :sis_id, String, "a id from the original SIS system", required: false
    end
    def account(id: nil, sis_id: nil)
      raise GraphQL::ExecutionError, "Must specify exactly one of id or sisId" if (id && sis_id) || !(id || sis_id)
      return GraphQLNodeLoader.load("Account", id, context) if id

      GraphQLNodeLoader.load("AccountBySis", sis_id, context) if sis_id
    end

    field :user, Types::UserType, null: true do
      argument :id,
               ID,
               "a graphql or legacy id",
               required: true,
               prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("User")
    end
    def user(id:)
      GraphQLNodeLoader.load("User", id, context)
    end

    field :course, Types::CourseType, null: true do
      argument :id,

View on GitHub (pinned to 1c9f0bb801)