{"record":{"id":"3f1648f80666c6e9","repo":"instructure/canvas-lms","slug":"unable-to-start-import-for-external-tool-tool-name-response","errorCode":null,"errorMessage":"Unable to start import for external tool #{@tool.name} (#{response.code})","messagePattern":"Unable to start import for external tool #(.+?) \\(#(.+?)\\)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"app/models/lti/content_migration_service/importer.rb","lineNumber":59,"sourceCode":"        @root_account = course.root_account\n        load_tool!\n        post_body = start_import_post_body(content)\n        response = Canvas.retriable(on: Timeout::Error) do\n          case import_format\n          when JSON_FORMAT\n            CanvasHttp.post(import_start_url, base_request_headers, body: post_body.to_json, content_type: \"application/json\")\n          else\n            CanvasHttp.post(import_start_url, base_request_headers, form_data: Rack::Utils.build_nested_query(post_body))\n          end\n        end\n        case response.code.to_i\n        when (200..201)\n          parsed_response = JSON.parse(response.body)\n          unless parsed_response.empty?\n            @status_url = parsed_response[\"status_url\"]\n          end\n        else\n          raise \"Unable to start import for external tool #{@tool.name} (#{response.code})\"\n        end\n        self\n      rescue Timeout::Error\n        raise \"Unable to start import for external tool #{@tool.name}, request timed out.\"\n      end\n\n      def import_completed?\n        InstrumentTLSCiphers.without_tls_metrics do\n          response = Canvas.retriable(on: Timeout::Error) { CanvasHttp.get(@status_url, base_request_headers) } if @status_url\n          if response&.code.to_i == 200\n            parsed_response = JSON.parse(response.body)\n            @export_status = parsed_response[\"status\"]\n            case @export_status\n            when SUCCESSFUL_STATUS\n              true\n            when FAILED_STATUS\n              raise parsed_response[\"message\"]\n            else","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/lti/content_migration_service/importer.rb#L41-L77","documentation":"Raised by Lti::ContentMigrationService::Importer#send_imported_content when the POST to the tool's content_migration.import_start_url returns an HTTP status outside 200-201. Canvas expected the tool to accept the imported content and respond with a status_url, but the tool rejected or failed the request. The tool name and HTTP status code are included to identify which integration and what kind of failure.","triggerScenarios":"send_imported_content posts the exported content to import_start_url and the tool replies 4xx/5xx (e.g. 401 bad OAuth signature, 404 wrong import_start_url, 500 tool crash). Any response.code not in (200..201) hits the raise.","commonSituations":"Tool's import_start_url misconfigured in its settings/XML; tool credentials or LTI shared secret changed so auth fails (401/403); tool endpoint removed or relocated after an upgrade (404); tool server error (5xx) under load; tool does not actually implement content migration import despite configuration.","solutions":["Check the HTTP code in the message: 401/403 → fix the tool's credentials/shared secret; 404 → fix import_start_url; 5xx → investigate the tool's server.","Verify the tool's content_migration.import_start_url in the ContextExternalTool settings matches the vendor's documented endpoint.","Confirm the tool still supports content migration (content_migration_configured?) and its configuration XML is current.","Retry the course import — transient 5xx may succeed on a second attempt.","Contact the tool vendor with the status code and timestamp if the configuration is correct."],"exampleFix":"# before: stale endpoint in tool config\nsettings: { content_migration: { import_start_url: 'https://tool.example.com/old/import' } }\n# after\nsettings: { content_migration: { import_start_url: 'https://tool.example.com/api/v2/import' } }","handlingStrategy":"validation","validationCode":"# before import, validate tool config and endpoint reachability\ntool = course.context_external_tools.find_by(id: original_tool_id)\nraise 'tool missing' unless tool&.content_migration_configured?\nimport_url = tool.settings.dig(:content_migration, :import_start_url)\nraise 'import_start_url missing' unless import_url.present?\nuri = URI(import_url)\nres = Net::HTTP.start(uri.host, uri.port, open_timeout: 5) { |h| h.head('/') }\nraise \"tool endpoint returned #{res.code}\" unless %w[200 201 302 401].include?(res.code) # 401 still proves host is up","typeGuard":null,"tryCatchPattern":"begin\n  importer.send_imported_content(course, migration, content)\nrescue RuntimeError => e\n  if e.message =~ /Unable to start import for external tool .+ \\((\\d+)\\)/\n    status = Regexp.last_match(1).to_i\n    case status\n    when 401, 403 then notify_admin('re-authenticate external tool')\n    when 404      then notify_admin('fix import_start_url configuration')\n    else               schedule_retry\n    end\n  else\n    raise\n  end\nend","preventionTips":["Keep the tool's content_migration.import_start_url in sync with vendor documentation on every tool upgrade.","Rotate/update LTI shared secrets promptly on both sides to avoid 401/403 responses.","Validate tool XML configuration after each vendor update.","Log response bodies (where safe) for the failing POST to diagnose status codes quickly."],"tags":["http","lti","external-tool","course-import","api-error"],"backgroundTag":"http-error-response","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"}