instructure/canvas-lms · error · RuntimeError
Unable to start import for external tool #
Error message
Unable to start import for external tool #{@tool.name}, request timed out. What it means
Raised by Lti::ContentMigrationService::Importer#send_imported_content when the initial POST of imported content to the tool's import_start_url raises Timeout::Error and Canvas.retriable's retries are exhausted. The import for the external tool never got a response, so no status_url exists and the import cannot proceed. Distinct from error 996: the tool never responded at all rather than returning an error status.
Solutions
- Retry the course import — transient timeouts often resolve.
- Verify the tool's import_start_url host is up and reachable (curl/ping from the Canvas server).
- Check network path: firewall rules, proxy config, DNS between Canvas and the tool host.
- Ask the tool vendor to make import asynchronous (return status_url immediately, process in background) if large payloads cause slow responses.
- Inspect Canvas and tool server logs around the timeout timestamp to see where the request stalled.
Example fix
null
Defensive patterns
Strategy: retry
Validate before calling
# verify the tool host accepts connections before posting content
uri = URI(import_start_url)
begin
Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) { |h| h.head('/') }
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError
raise 'tool host unreachable; skip import attempt now'
end Try / catch
begin
importer.send_imported_content(course, migration, content)
rescue RuntimeError => e
if e.message.include?('request timed out')
# exponential backoff retry, then surface a user-facing 'tool unavailable' message
retry_later_with_backoff(importer)
else
raise
end
end Prevention
- Health-check the tool host before launching course copies/imports.
- Prefer vendors with asynchronous import endpoints that return status_url immediately.
- Watch for network changes (proxies, firewalls) between Canvas and tool hosts.
- Set retry budgets so a dead tool fails fast instead of stalling the import job.
When it happens
Trigger: send_imported_content's CanvasHttp.post(import_start_url, ...) times out (after Canvas.retriable retries on Timeout::Error) — typically the tool host is unreachable, hung, or too slow processing the posted content payload.
Common situations: Tool server down or overloaded during a course copy; large content payloads causing long tool-side processing; network partition/firewall silently dropping packets to the tool host; DNS failures in self-hosted environments; tool's import endpoint doing synchronous heavy work.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- could not retrieve configuration, the server response timed…
- Fetching data from #
- Couldn't send LTI AGS grade progress metric
- failed to load page view history due to service timeout
- Unable to fetch export data from #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/15a1516530c889bd.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/lti/content_migration_service/importer.rb:63
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"]
else
false
end
end
endView on GitHub (pinned to 1c9f0bb801)