{"record":{"id":"a89240fd2febb825","repo":"instructure/canvas-lms","slug":"no-matching-external-tool-found","errorCode":null,"errorMessage":"no matching external tool found","messagePattern":"no matching external tool found","errorType":"exception","errorClass":"SubmissionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/create_submission_draft.rb","lineNumber":70,"sourceCode":"      submission:,\n      submission_attempt: input[:attempt] || (submission.attempt + 1)\n    ).first_or_create!\n\n    # TODO: we should research if we should split this mutation into a separate\n    #       mutation for each draft type. the primary concern is the confusion\n    #       of ignoring potentially included input types if they don't match\n    #       the active submission.\n    submission_draft.active_submission_type = input[:active_submission_type]\n    case input[:active_submission_type]\n    when \"basic_lti_launch\"\n      raise SubmissionError if input[:lti_launch_url].blank? || input[:external_tool_id].blank?\n\n      external_tool = Lti::ToolFinder.from_url(\n        input[:lti_launch_url],\n        submission.course,\n        preferred_tool_id: input[:external_tool_id]\n      )\n      raise SubmissionError, I18n.t(\"no matching external tool found\") if external_tool.blank?\n\n      submission_draft.context_external_tool_id = external_tool.id\n      submission_draft.lti_launch_url = input[:lti_launch_url]\n      submission_draft.resource_link_lookup_uuid = input[:resource_link_lookup_uuid]\n    when \"media_recording\"\n      submission_draft.media_object_id = input[:media_id]\n    when \"online_text_entry\"\n      submission_draft.body = input[:body]\n    when \"online_upload\"\n      file_ids = (input[:file_ids] || []).compact.uniq\n      attachments = get_and_verify_attachments!(file_ids)\n      verify_allowed_extensions!(submission.assignment, attachments)\n      submission_draft.attachments = attachments\n    when \"online_url\"\n      submission_draft.url = input[:url]\n    end\n\n    submission_draft.save!","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/create_submission_draft.rb#L52-L88","documentation":"In CreateSubmissionDraft, when the submission type is basic_lti_launch the mutation resolves the configured LTI tool via Lti::ToolFinder.from_url using the provided lti_launch_url. If no installed ContextExternalTool in the course matches that URL (and preferred_tool_id), it raises SubmissionError with \"no matching external tool found\".","triggerScenarios":"Submitting a draft with submission_types including basic_lti_launch and an lti_launch_url that no installed external tool in the course covers; passing an external_tool_id that does not match or is not deployed in the course; tool configured at account level but not visible to the course; URL changed after the tool was configured.","commonSituations":"LTI 1.3 tool moved to a new launch URL without updating Canvas config; developer uses a dev/test tool URL on a different environment; tool deleted or uninstalled from the course/account; typo or trailing path difference in the launch URL.","solutions":["Confirm an external tool with a URL matching lti_launch_url is installed in the course (or its account), e.g. check course.context_external_tools.","Pass the correct external_tool_id matching the deployed tool.","Fix the lti_launch_url to exactly match the tool's configured domain/path.","If the tool config changed, re-deploy/re-install the external tool (XML or LTI 1.3 registration) for this course or account."],"exampleFix":"// before\nmutation.draft(submissionId: id, ltiLaunchUrl: 'https://old-tool.example.com/launch')\n// after\n# ensure tool installed, then pass matching id\nmutation.draft(submissionId: id, ltiLaunchUrl: 'https://tool.example.com/launch', externalToolId: tool.id)","handlingStrategy":"validation","validationCode":"const tools = course.contextExternalTools.nodes || []\nconst match = tools.find(t => ltiLaunchUrl.includes(t.domain))\nif (!match) throw new SkipError('no matching external tool for ' + ltiLaunchUrl)","typeGuard":"function hasMatchingTool(tools, url) { return Array.isArray(tools) && tools.some(t => typeof t.domain === 'string' && url.includes(t.domain)); }","tryCatchPattern":"try {\n  await createSubmissionDraft(...)\n} catch (e) {\n  if (e.message.includes('no matching external tool found')) {\n    showLtiToolMissingDialog(ltiLaunchUrl)\n  } else throw e;\n}","preventionTips":["Keep LTI tool launch URLs in config in sync with installed tool definitions.","Prefer passing external_tool_id alongside lti_launch_url.","Verify tool placement includes homework_submission/assignment_selection.","Reinstall the tool after URL changes."],"tags":["graphql","lti","external-tool"],"backgroundTag":"resource-not-found","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"}