instructure/canvas-lms · error · RuntimeError
Unable to start import for external tool #
Error message
Unable to start import for external tool #{@tool.name} (#{response.code}) What it means
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.
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.
Example fix
# before: stale endpoint in tool config
settings: { content_migration: { import_start_url: 'https://tool.example.com/old/import' } }
# after
settings: { content_migration: { import_start_url: 'https://tool.example.com/api/v2/import' } } Defensive patterns
Strategy: validation
Validate before calling
# before import, validate tool config and endpoint reachability
tool = course.context_external_tools.find_by(id: original_tool_id)
raise 'tool missing' unless tool&.content_migration_configured?
import_url = tool.settings.dig(:content_migration, :import_start_url)
raise 'import_start_url missing' unless import_url.present?
uri = URI(import_url)
res = Net::HTTP.start(uri.host, uri.port, open_timeout: 5) { |h| h.head('/') }
raise "tool endpoint returned #{res.code}" unless %w[200 201 302 401].include?(res.code) # 401 still proves host is up Try / catch
begin
importer.send_imported_content(course, migration, content)
rescue RuntimeError => e
if e.message =~ /Unable to start import for external tool .+ \((\d+)\)/
status = Regexp.last_match(1).to_i
case status
when 401, 403 then notify_admin('re-authenticate external tool')
when 404 then notify_admin('fix import_start_url configuration')
else schedule_retry
end
else
raise
end
end Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Unable to find external tool to import content.
- Unable to start import for external tool #
- Content-Type must be 'application/xml'
- could not retrieve configuration, the server response timed…
- Couldn't send LTI AGS grade progress metric
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/3f1648f80666c6e9.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/lti/content_migration_service/importer.rb:59
@root_account = course.root_account
load_tool!
post_body = start_import_post_body(content)
response = Canvas.retriable(on: Timeout::Error) do
case import_format
when JSON_FORMAT
CanvasHttp.post(import_start_url, base_request_headers, body: post_body.to_json, content_type: "application/json")
else
CanvasHttp.post(import_start_url, base_request_headers, form_data: Rack::Utils.build_nested_query(post_body))
end
end
case response.code.to_i
when (200..201)
parsed_response = JSON.parse(response.body)
unless parsed_response.empty?
@status_url = parsed_response["status_url"]
end
else
raise "Unable to start import for external tool #{@tool.name} (#{response.code})"
end
self
rescue Timeout::Error
raise "Unable to start import for external tool #{@tool.name}, request timed out."
end
def import_completed?
InstrumentTLSCiphers.without_tls_metrics do
response = Canvas.retriable(on: Timeout::Error) { CanvasHttp.get(@status_url, base_request_headers) } if @status_url
if response&.code.to_i == 200
parsed_response = JSON.parse(response.body)
@export_status = parsed_response["status"]
case @export_status
when SUCCESSFUL_STATUS
true
when FAILED_STATUS
raise parsed_response["message"]
elseView on GitHub (pinned to 1c9f0bb801)