instructure/canvas-lms · error · RuntimeError

Error sending import for Outcomes Service: #

Error message

Error sending import for Outcomes Service: #{response.body}

What it means

This error is raised by OutcomesService::MigrationService#send_imported_content when the HTTP POST of exported course content to the Outcomes Service returns a non-2xx status code. It propagates the raw response body of the failed HTTP call so the developer can see what the remote service rejected. It is a deliberate fail-fast guard after the CanvasHttp.post call.

Solutions

  1. Check the response body in the error message to see the exact HTTP error the Outcomes Service returned
  2. Verify OutcomesService::Service.url resolves to the correct, reachable Outcomes Service host for the shard/account
  3. Confirm the JWT/credentials used for the service-to-service call are valid and not expired
  4. Reproduce the POST manually (curl) with the same payload to isolate payload vs auth vs connectivity issues
  5. Retry the content migration once Outcomes Service is healthy; check its logs for the matching request

Example fix

// before
raise "Error sending import for Outcomes Service: #{response.body}"
// after
unless /^2/.match?(response.code.to_s)
  Canvas::Errors.capture("outcomes_service_import_failed", body: response.body, code: response.code)
  raise "Error sending import for Outcomes Service (HTTP #{response.code}): #{response.body}"
end
Defensive patterns

Strategy: retry

Validate before calling

uri = "#{OutcomesService::Service.url(course)}/api/content_imports"
raise "Outcomes Service URL not configured" if uri.blank?
# pre-check service health
health = CanvasHttp.get("#{OutcomesService::Service.url(course)}/health")
raise "Outcomes Service unreachable" unless /^2/.match?(health.code.to_s)

Type guard

valid = response.respond_to?(:code) && /^2/.match?(response.code.to_s)

Try / catch

begin
  service.send_imported_content(course, content_migration, content_export)
rescue RuntimeError => e
  Rails.logger.error("Outcomes Service import failed: #{e.message}")
  Canvas::Errors.capture(e)
  content_migration.fail!
end

Prevention

When it happens

Trigger: The POST to the Outcomes Service content_imports API returns a 4xx/5xx code: invalid or expired JWT in the Authorization header, payload the service rejects (bad JSON or disallowed keys), the Outcomes Service host is down/misconfigured (OutcomesService::Service.url wrong), or a proxy/gateway returns 502/503.

Common situations: Outcomes Service instance not deployed or at wrong URL in dynamic_settings/config; service account credentials/JWT signing keys mismatched after rotation; large content migrations timing out behind a load balancer; running an older Outcomes Service version that lacks an endpoint the Canvas side calls.

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


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/414f765549612ca4. Report an issue: GitHub.

Appendix: source

Thrown at app/models/outcomes_service/migration_service.rb:113

          external_migration_id: content_migration.id
        )
        extractor = OutcomesService::MigrationExtractor.new(content_migration)
        data = data.merge(
          outcomes: extractor.learning_outcomes(course),
          groups: extractor.learning_outcome_groups(course),
          edges: extractor.learning_outcome_links
        )
        response = CanvasHttp.post(
          content_imports_url,
          headers_for(course, "content_migration.import", context_type: "course", context_id: course.id.to_s),
          body: data.to_json,
          content_type: "application/json"
        )
        if /^2/.match?(response.code.to_s)
          json = JSON.parse(response.body)
          { import_id: json["id"], course:, content_migration: }
        else
          raise "Error sending import for Outcomes Service: #{response.body}"
        end
      end

      def import_completed?(import_data)
        content_import_url = "#{OutcomesService::Service.url(import_data[:course])}/api/content_imports/#{import_data[:import_id]}"
        response = CanvasHttp.get(
          content_import_url,
          headers_for(import_data[:course], "content_migration.import", id: import_data[:import_id])
        )
        if /^2/.match?(response.code.to_s)
          json = JSON.parse(response.body)
          json["missing_alignments"]&.each do |missing_alignment|
            page = lookup_artifact(missing_alignment["artifact_type"],
                                   missing_alignment["artifact_id"],
                                   import_data[:course])
            if page.nil?
              import_data[:content_migration].add_warning(I18n.t("Unable to align some outcomes to a page"))
            else

View on GitHub (pinned to 1c9f0bb801)