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
- Check the response body in the error message to see the exact HTTP error the Outcomes Service returned
- Verify OutcomesService::Service.url resolves to the correct, reachable Outcomes Service host for the shard/account
- Confirm the JWT/credentials used for the service-to-service call are valid and not expired
- Reproduce the POST manually (curl) with the same payload to isolate payload vs auth vs connectivity issues
- 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
- Monitor Outcomes Service health/uptime before large migrations
- Validate JWT signing config for service-to-service auth after key rotation
- Log full response code and body on failure for triage
- Add retry with backoff for transient 5xx responses
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
- Content Export for Outcomes Service failed
- # : #
- Error parsing JSON results from Outcomes Service: #
- Error queueing export for Outcomes Service: #
- Error retrieving export for Outcomes Service: #
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"))
elseView on GitHub (pinned to 1c9f0bb801)