instructure/canvas-lms · error
e.message
Error message
e.message
What it means
The LTI AGS Line Items controller rescues ActionController::BadRequest and renders {"error": e.message} with HTTP 400. ActionController::BadRequest is raised when Rails fails to parse or validate the request itself — most often a malformed JSON body that the wrapped_params parse cannot decode, or a query/body parameter that fails coercion. Because the message is the raw exception message, the JSON payload can be a verbose parser error rather than a clean AGS error code.
Solutions
- Inspect the rendered e.message in the 400 response (or server logs outside production, where it is also logged) — it usually contains the JSON parser offset or missing parameter name.
- Ensure the request sets Content-Type: application/json and the body is well-formed JSON containing scoreMaximum and label per AGS spec.
- Validate/JSON.stringify the payload in the tool client before sending, and reject NaN/undefined scoreMaximum values.
- If a Canvas upgrade changed accepted params, diff against the AGS spec and update the tool's request body.
Example fix
// before
fetch(lineItemsUrl, {
method: 'POST',
body: JSON.stringify({ scoreMaximum: '100' }) // no Content-Type header
});
// after
fetch(lineItemsUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ scoreMaximum: 100, label: 'Quiz 1', resourceId: 'quiz-1' })
}); Defensive patterns
Strategy: validation
Validate before calling
function validateLineItemPayload(payload) {
if (typeof payload.scoreMaximum !== 'number' || Number.isNaN(payload.scoreMaximum)) throw new Error('scoreMaximum must be a finite number');
if (typeof payload.label !== 'string' || payload.label.length === 0) throw new Error('label is required');
JSON.stringify(payload); // throws on cycles/non-serializable values
return payload;
} Type guard
function isLineItem(v) {
return typeof v === 'object' && v !== null && typeof v.scoreMaximum === 'number' && Number.isFinite(v.scoreMaximum) && typeof v.label === 'string';
} Try / catch
try {
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (res.status === 400) {
const msg = (await res.json()).error;
console.warn('AGS rejected request:', msg); // raw parser/param message
}
} catch (e) {
handleNetworkError(e);
} Prevention
- Always send Content-Type: application/json for AGS line item calls.
- Run the body through JSON.parse(JSON.stringify(payload)) in tests to catch malformed serialization early.
- Send AGS requests through one small client wrapper that validates required fields (scoreMaximum, label) before the call.
- After Canvas upgrades, re-run AGS integration tests — accepted strong-params may have changed.
When it happens
Trigger: POST/PUT to /api/lti/courses/:course_id/line_items with a request body that is not valid JSON (truncated body, wrong Content-Type, single quotes, trailing commas), or required AGS fields (e.g. scoreMaximum, label) absent so strong-parameters/wrapped param handling raises BadRequest.
Common situations: Tool platform serializing the AGS line-item payload with wrong Content-Type (form-urlencoded instead of application/json); sending an empty body; proxy or gateway truncating the body; client library version emitting a body shape the endpoint's strong params no longer accept after a Canvas upgrade.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
- Invalid submission_type for new assignment: #
- Couldn't send LTI AGS grade progress metric
- invalid_grant
- Access token expired
- Access token invalid - signature likely incorrect
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/c7bf0378838df0a6.
Report an issue: GitHub.
Appendix: source
Thrown at app/controllers/lti/ims/line_items_controller.rb:98
ACTION_SCOPE_MATCHERS = {
create: all_of(TokenScopes::LTI_AGS_LINE_ITEM_SCOPE),
update: all_of(TokenScopes::LTI_AGS_LINE_ITEM_SCOPE),
destroy: all_of(TokenScopes::LTI_AGS_LINE_ITEM_SCOPE),
show: any_of(TokenScopes::LTI_AGS_LINE_ITEM_SCOPE, TokenScopes::LTI_AGS_LINE_ITEM_READ_ONLY_SCOPE),
index: any_of(TokenScopes::LTI_AGS_LINE_ITEM_SCOPE, TokenScopes::LTI_AGS_LINE_ITEM_READ_ONLY_SCOPE)
}.with_indifferent_access.freeze
MIME_TYPE = "application/vnd.ims.lis.v2.lineitem+json"
CONTAINER_MIME_TYPE = "application/vnd.ims.lis.v2.lineitemcontainer+json"
rescue_from ActionController::BadRequest do |e|
unless Rails.env.production?
logger.error(e.message)
Lti::Errors::ErrorLogger.log_error(e)
end
render json: { error: e.message }, status: :bad_request
end
# @API Create a Line Item
# Create a new Line Item
#
# @argument scoreMaximum [Required, Float]
# The maximum score for the line item. Scores created for the Line Item may exceed this value.
#
# @argument label [Required, String]
# The label for the Line Item. If no resourceLinkId is specified this value will also be used
# as the name of the placeholder assignment.
#
# @argument resourceId [String]
# A Tool Provider specified id for the Line Item. Multiple line items may
# share the same resourceId within a given context.
#
# @argument tag [String]
# A value used to qualify a line Item beyond its ids. Line Items may be queried
# by this value in the List endpoint. Multiple line items can share the same tagView on GitHub (pinned to 1c9f0bb801)