instructure/canvas-lms · error · BasicLTI::BasicOutcomes::InvalidRequest
Content-Type must be 'application/xml'
Error message
Content-Type must be 'application/xml'
What it means
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.
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
Example fix
// before
fetch(url, { method: 'POST', body: xmlString })
// after
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlString }) Defensive patterns
Strategy: validation
Validate before calling
if (!xmlString || xmlString.trim().startsWith('{')) throw new Error('Body must be LTI Basic Outcomes XML');
opts.headers['Content-Type'] = 'application/xml'; Type guard
const isXml = (s) => typeof s === 'string' && /<\?xml|<imsx_POXEnvelopeRequest/.test(s.trim());
Try / catch
begin
post(url, xml_body, headers: { 'Content-Type' => 'application/xml' })
rescue BasicLTI::BasicOutcomes::InvalidRequest => e
log_warn("outcomes rejected: #{e.message}")
end Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- could not retrieve configuration, the server response timed…
- Couldn't send LTI AGS grade progress metric
- Fetching data from #
- Missing Content-Type header in response.
- Missing Content-Type header in response.
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/45762944c9d45bfb.
Report an issue: GitHub.
Appendix: source
Thrown at app/controllers/lti_api_controller.rb:38
require "oauth"
require "oauth/client/action_controller_request"
require "nokogiri"
class LtiApiController < ApplicationController
skip_before_action :load_user, :require_user
skip_before_action :verify_authenticity_token
# these exceptions will happen on bad external requests,
# we don't need to tell sentry about every one of them
rescue_from BasicLTI::BasicOutcomes::Unauthorized, BasicLTI::BasicOutcomes::InvalidRequest, with: :rescue_expected_error_type
# this API endpoint passes all the existing tests for the LTI v1.1 outcome service specification
def grade_passback
verify_oauth
if request.media_type != "application/xml"
raise BasicLTI::BasicOutcomes::InvalidRequest, "Content-Type must be 'application/xml'"
end
@xml = Nokogiri::XML.parse(request.body)
lti_response, status = check_outcome BasicLTI::BasicOutcomes.process_request(@tool, @xml)
# Log asset access for participation tracking
if lti_response && lti_response.operation_ref_identifier == "replaceResult" && lti_response.code_major == "success"
begin
assignment = lti_response.assignment
user = lti_response.user
@context = assignment.context
@current_user = user
log_asset_access(assignment, "assignments", assignment.assignment_group, "participate")
rescue
# Don't fail the grade passback if asset logging fails
end
endView on GitHub (pinned to 1c9f0bb801)