instructure/canvas-lms · error · Common::InvalidResultError
Result format is invalid: #
Error message
Result format is invalid: #{content_type} What it means
PageViews::FetchResultService#determine_result_format raises Common::InvalidResultError when the Content-Type returned by the async page-views results endpoint does not match an entry in PageViews::Common::CONTENT_TYPE_MAPPINGS (only 'application/jsonl' and 'text/csv' are supported). The service only knows how to hand back csv or jsonl download payloads, so any other MIME type means the upstream service returned something unexpected and the result cannot be interpreted safely.
Solutions
- Log response.header['Content-Type'] and the query_id, then inspect the actual response body to see what the upstream service really returned
- Verify the PageViews configuration URI points at the real async page-views API, not a proxy, SSO page, or wrong environment
- Confirm the query completed successfully (poll status == 'finished') before fetching results; a failed/running query can return a non-file body
- If the upstream legitimately added a new format, add it to PageViews::Common::CONTENT_TYPE_MAPPINGS in app/services/page_views/common.rb and handle the new format downstream
Example fix
# before
format = FetchResultService.new(config, requestor_user: user).call(query_id) # raises on unexpected Content-Type
# after
result = FetchResultService.new(config, requestor_user: user).call(query_id)
rescue PageViews::Common::InvalidResultError => e
Rails.logger.warn("page_views result format rejected (#{e.message}), query=#{query_id}")
redirect_to page_views_query_path(query_id), alert: t("result_unavailable") Defensive patterns
Strategy: try-catch
Validate before calling
# guard before consuming the result result = FetchResultService.new(config, requestor_user: user) rescue PageViews::Common::InvalidResultError # fall back to re-polling status or surfacing an error end
Type guard
def supported_content_type?(response)
ct = response.header["Content-Type"].to_s.split(";").first.to_s.strip
PageViews::Common::CONTENT_TYPE_MAPPINGS.key?(ct)
end Try / catch
begin
result = service.call(query_id)
rescue PageViews::Common::InvalidResultError => e
Rails.logger.warn("unexpected page_views content-type: #{e.message}")
render json: { error: "result_format_unavailable" }, status: :bad_gateway
end Prevention
- Poll query status until 'finished' before fetching results
- Keep proxy/CDN layers from rewriting Content-Type on the results endpoint
- When adding new export formats upstream, update CONTENT_TYPE_MAPPINGS in the same change
- Log Content-Type and body snippet on failure for quick diagnosis
When it happens
Trigger: Calling PageViews::FetchResultService#call(query_id) after a query finishes and the results response has a Content-Type other than application/jsonl or text/csv (after stripping '; parameters'), e.g. text/html from an auth/SSO redirect page, application/json error payload, or text/plain.
Common situations: The page-views service URL points at a proxy/login page instead of the API (misconfigured base URI); the query failed upstream and an HTML/JSON error body was returned with 200; the upstream service changed or added a new export format (e.g. application/parquet) not yet mapped in CONTENT_TYPE_MAPPINGS.
Related errors
- Content-Type must be 'application/xml'
- Error retrieving results from Outcomes Service: #
- invalid request
- 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/2d3515a0039eaddd.
Report an issue: GitHub.
Appendix: source
Thrown at app/services/page_views/fetch_result_service.rb:44
request_headers
) do |response|
handle_generic_errors(response) unless response.code.to_i == 200
response.decode_content = false # Prevent automatic decompression
format = determine_result_format(response)
filename = determine_filename(response).delete_suffix(".gz")
compressed = response_compressed?(response)
return Common::DownloadableResult.new(format:, filename:, content: response.body, compressed?: compressed)
end
end
private
def determine_result_format(response)
raise InvalidResultError, "Missing Content-Type header in response." unless response.header["Content-Type"]
# strip any parameters (encoding for example) from the Content-Type
content_type = response.header["Content-Type"].split(";").first.strip
raise Common::InvalidResultError, "Result format is invalid: #{content_type}" unless Common::CONTENT_TYPE_MAPPINGS[content_type]
Common::CONTENT_TYPE_MAPPINGS[content_type]
end
def determine_filename(response)
content_disposition = response.header["Content-Disposition"]
if content_disposition && content_disposition =~ /filename="?([^";]+)"?/
Regexp.last_match(1)
else
raise Common::InvalidResultError, "Unable to determine filename from Content-Disposition header"
end
end
def response_compressed?(response)
(response.header["Content-Encoding"] && response.header["Content-Encoding"] == "gzip") ||
determine_filename(response).end_with?(".gz")
end
endView on GitHub (pinned to 1c9f0bb801)