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

  1. 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.
  2. Ensure the request sets Content-Type: application/json and the body is well-formed JSON containing scoreMaximum and label per AGS spec.
  3. Validate/JSON.stringify the payload in the tool client before sending, and reject NaN/undefined scoreMaximum values.
  4. 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

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


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 tag

View on GitHub (pinned to 1c9f0bb801)