{"record":{"id":"414f765549612ca4","repo":"instructure/canvas-lms","slug":"error-sending-import-for-outcomes-service-response-body","errorCode":null,"errorMessage":"Error sending import for Outcomes Service: #{response.body}","messagePattern":"Error sending import for Outcomes Service: #(.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"app/models/outcomes_service/migration_service.rb","lineNumber":113,"sourceCode":"          external_migration_id: content_migration.id\n        )\n        extractor = OutcomesService::MigrationExtractor.new(content_migration)\n        data = data.merge(\n          outcomes: extractor.learning_outcomes(course),\n          groups: extractor.learning_outcome_groups(course),\n          edges: extractor.learning_outcome_links\n        )\n        response = CanvasHttp.post(\n          content_imports_url,\n          headers_for(course, \"content_migration.import\", context_type: \"course\", context_id: course.id.to_s),\n          body: data.to_json,\n          content_type: \"application/json\"\n        )\n        if /^2/.match?(response.code.to_s)\n          json = JSON.parse(response.body)\n          { import_id: json[\"id\"], course:, content_migration: }\n        else\n          raise \"Error sending import for Outcomes Service: #{response.body}\"\n        end\n      end\n\n      def import_completed?(import_data)\n        content_import_url = \"#{OutcomesService::Service.url(import_data[:course])}/api/content_imports/#{import_data[:import_id]}\"\n        response = CanvasHttp.get(\n          content_import_url,\n          headers_for(import_data[:course], \"content_migration.import\", id: import_data[:import_id])\n        )\n        if /^2/.match?(response.code.to_s)\n          json = JSON.parse(response.body)\n          json[\"missing_alignments\"]&.each do |missing_alignment|\n            page = lookup_artifact(missing_alignment[\"artifact_type\"],\n                                   missing_alignment[\"artifact_id\"],\n                                   import_data[:course])\n            if page.nil?\n              import_data[:content_migration].add_warning(I18n.t(\"Unable to align some outcomes to a page\"))\n            else","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/outcomes_service/migration_service.rb#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nraise \"Error sending import for Outcomes Service: #{response.body}\"\n// after\nunless /^2/.match?(response.code.to_s)\n  Canvas::Errors.capture(\"outcomes_service_import_failed\", body: response.body, code: response.code)\n  raise \"Error sending import for Outcomes Service (HTTP #{response.code}): #{response.body}\"\nend","handlingStrategy":"retry","validationCode":"uri = \"#{OutcomesService::Service.url(course)}/api/content_imports\"\nraise \"Outcomes Service URL not configured\" if uri.blank?\n# pre-check service health\nhealth = CanvasHttp.get(\"#{OutcomesService::Service.url(course)}/health\")\nraise \"Outcomes Service unreachable\" unless /^2/.match?(health.code.to_s)","typeGuard":"valid = response.respond_to?(:code) && /^2/.match?(response.code.to_s)","tryCatchPattern":"begin\n  service.send_imported_content(course, content_migration, content_export)\nrescue RuntimeError => e\n  Rails.logger.error(\"Outcomes Service import failed: #{e.message}\")\n  Canvas::Errors.capture(e)\n  content_migration.fail!\nend","preventionTips":["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"],"tags":["http","outcomes-service","api-integration"],"backgroundTag":"http-error-response","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}