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

  1. Retry the course import — transient timeouts often resolve.
  2. Verify the tool's import_start_url host is up and reachable (curl/ping from the Canvas server).
  3. Check network path: firewall rules, proxy config, DNS between Canvas and the tool host.
  4. Ask the tool vendor to make import asynchronous (return status_url immediately, process in background) if large payloads cause slow responses.
  5. 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

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.

Related errors


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
        end

View on GitHub (pinned to 1c9f0bb801)