{"record":{"id":"45762944c9d45bfb","repo":"instructure/canvas-lms","slug":"content-type-must-be-application-xml","errorCode":null,"errorMessage":"Content-Type must be 'application/xml'","messagePattern":"Content-Type must be 'application/xml'","errorType":"exception","errorClass":"BasicLTI::BasicOutcomes::InvalidRequest","httpStatus":400,"severity":"error","filePath":"app/controllers/lti_api_controller.rb","lineNumber":38,"sourceCode":"\nrequire \"oauth\"\nrequire \"oauth/client/action_controller_request\"\nrequire \"nokogiri\"\n\nclass LtiApiController < ApplicationController\n  skip_before_action :load_user, :require_user\n  skip_before_action :verify_authenticity_token\n\n  # these exceptions will happen on bad external requests,\n  # we don't need to tell sentry about every one of them\n  rescue_from BasicLTI::BasicOutcomes::Unauthorized, BasicLTI::BasicOutcomes::InvalidRequest, with: :rescue_expected_error_type\n\n  # this API endpoint passes all the existing tests for the LTI v1.1 outcome service specification\n  def grade_passback\n    verify_oauth\n\n    if request.media_type != \"application/xml\"\n      raise BasicLTI::BasicOutcomes::InvalidRequest, \"Content-Type must be 'application/xml'\"\n    end\n\n    @xml = Nokogiri::XML.parse(request.body)\n\n    lti_response, status = check_outcome BasicLTI::BasicOutcomes.process_request(@tool, @xml)\n\n    # Log asset access for participation tracking\n    if lti_response && lti_response.operation_ref_identifier == \"replaceResult\" && lti_response.code_major == \"success\"\n      begin\n        assignment = lti_response.assignment\n        user = lti_response.user\n        @context = assignment.context\n        @current_user = user\n        log_asset_access(assignment, \"assignments\", assignment.assignment_group, \"participate\")\n      rescue\n        # Don't fail the grade passback if asset logging fails\n      end\n    end","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/controllers/lti_api_controller.rb#L20-L56","documentation":"Raised in LtiApiController#grade_passback when the LTI 1.1 Outcomes (grade passback) request is not sent with Content-Type application/xml. The Basic Outcomes processor expects the OAuth-signed XML envelope per the IMS spec, so any other media type is rejected as an invalid request before parsing.","triggerScenarios":"Tool sends a replaceResult/readResult/deleteResult request with missing, JSON, form-encoded, or text Content-Type headers; HTTP client defaults to form-urlencoded when posting the XML body; proxies/gateways rewriting the Content-Type.","commonSituations":"Integrating an LTI 1.1 tool whose grade passback was written casually (e.g. fetch without explicit headers); testing with curl without -H 'Content-Type: application/xml'; migrating code to a client that serializes JSON by default.","solutions":["Set the Content-Type header to application/xml on the outcomes POST","Configure the HTTP client to not override the header (e.g. don't pass JSON bodies/objects)","Re-test with curl -H 'Content-Type: application/xml' --data-binary @envelope.xml","Ensure any intermediary proxy preserves the Content-Type"],"exampleFix":"// before\nfetch(url, { method: 'POST', body: xmlString })\n// after\nfetch(url, { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlString })","handlingStrategy":"validation","validationCode":"if (!xmlString || xmlString.trim().startsWith('{')) throw new Error('Body must be LTI Basic Outcomes XML');\nopts.headers['Content-Type'] = 'application/xml';","typeGuard":"const isXml = (s) => typeof s === 'string' && /<\\?xml|<imsx_POXEnvelopeRequest/.test(s.trim());","tryCatchPattern":"begin\n  post(url, xml_body, headers: { 'Content-Type' => 'application/xml' })\nrescue BasicLTI::BasicOutcomes::InvalidRequest => e\n  log_warn(\"outcomes rejected: #{e.message}\")\nend","preventionTips":["Always set Content-Type: application/xml explicitly","Test passback with curl and a captured envelope","Don't let JSON HTTP clients auto-serialize the body"],"tags":["lti","http","content-type"],"backgroundTag":"invalid-argument-format","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"}