instructure/canvas-lms · error · OSFetchError

Error parsing JSON results from Outcomes Service: #

Error message

Error parsing JSON results from Outcomes Service: #{response.body}

What it means

After a 2xx response from the Outcomes Service, get_request_page parses the body as JSON and normalizes result attempts. If JSON.parse (and the subsequent shape handling) raises, it is rescued and re-raised as OSFetchError including the raw response body, because a 200 with unparseable/missing content still yields no usable results.

Solutions

  1. Log/inspect the response body included in the error to see what actually came back
  2. Verify the Outcomes Service version matches the response schema Canvas expects
  3. Check for proxies (LB, auth middleware) returning HTML with 200 status
  4. Add a schema/validation step before parsing to fail fast with a clearer message

Example fix

// before
rescue
  raise OSFetchError, "Error parsing JSON results from Outcomes Service: #{response.body}"
// after
rescue JSON::ParserError => e
  raise OSFetchError, "Error parsing JSON results from Outcomes Service (#{e.message}): #{response.body[0, 500]}"
Defensive patterns

Strategy: try-catch

Validate before calling

# validate body looks like JSON before parsing
raise OSFetchError, 'empty body' if response.body.to_s.strip.empty?
JSON.parse(response.body) # raises JSON::ParserError with position info

Try / catch

begin
  parsed = JSON.parse(response.body)
rescue JSON::ParserError => e
  raise OSFetchError, "Error parsing JSON results from Outcomes Service: #{e.message} #{response.body[0, 200]}"
end

Prevention

When it happens

Trigger: Outcomes Service returns a 2xx whose body is not valid JSON (HTML error page from a proxy, empty body, truncated response) or valid JSON with an unexpected shape (missing 'results'/'attempts' keys where deep_symbolize_keys/normalization is applied).

Common situations: Load balancer or auth proxy intercepting and returning an HTML 200 page; outcomes service bug emitting malformed JSON; API version drift changing the response envelope.

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/8beaad9c271dea0a. Report an issue: GitHub.

Appendix: source

Thrown at app/helpers/canvas_outcomes_helper.rb:132

          results = response_parser_callback.call(response)
        else
          results = JSON.parse(response.body).deep_symbolize_keys[:results]
          results.each do |result|
            next if result[:attempts].nil?

            result[:attempts].each do |attempt|
              # Initially metadata was a string, now it's a jsonb data type. When it was a string, canvas needed
              # to parse the result returned from outcome service
              next unless attempt[:metadata].is_a? String

              attempt[:metadata] = JSON.parse(attempt[:metadata]) unless attempt[:metadata].nil?
              attempt[:metadata] = attempt[:metadata].deep_symbolize_keys unless attempt[:metadata].nil?
            end
          end
        end
        { results:, total_pages: }
      rescue
        raise OSFetchError, "Error parsing JSON results from Outcomes Service: #{response.body}"
      end
    else
      raise OSFetchError, "Error retrieving results from Outcomes Service: #{response.body}"
    end
  end

  def outcome_has_alignments?(outcome, context)
    response = get_outcome_alignments(context, outcome.id, { includes: "alignments" })
    return false if response.nil?

    response.first[:alignments].count > 0
  end

  def outcome_has_authoritative_results?(outcome, context)
    assignments = Assignment.active.where(context:).quiz_lti

    return false if assignments.blank?

View on GitHub (pinned to 1c9f0bb801)