{"record":{"id":"2d3515a0039eaddd","repo":"instructure/canvas-lms","slug":"result-format-is-invalid-content-type-fetch-service","errorCode":null,"errorMessage":"Result format is invalid: #{content_type}","messagePattern":"Result format is invalid: #(.+?)","errorType":"exception","errorClass":"Common::InvalidResultError","httpStatus":null,"severity":"error","filePath":"app/services/page_views/fetch_result_service.rb","lineNumber":44,"sourceCode":"        request_headers\n      ) do |response|\n        handle_generic_errors(response) unless response.code.to_i == 200\n        response.decode_content = false # Prevent automatic decompression\n        format = determine_result_format(response)\n        filename = determine_filename(response).delete_suffix(\".gz\")\n        compressed = response_compressed?(response)\n        return Common::DownloadableResult.new(format:, filename:, content: response.body, compressed?: compressed)\n      end\n    end\n\n    private\n\n    def determine_result_format(response)\n      raise InvalidResultError, \"Missing Content-Type header in response.\" unless response.header[\"Content-Type\"]\n\n      # strip any parameters (encoding for example) from the Content-Type\n      content_type = response.header[\"Content-Type\"].split(\";\").first.strip\n      raise Common::InvalidResultError, \"Result format is invalid: #{content_type}\" unless Common::CONTENT_TYPE_MAPPINGS[content_type]\n\n      Common::CONTENT_TYPE_MAPPINGS[content_type]\n    end\n\n    def determine_filename(response)\n      content_disposition = response.header[\"Content-Disposition\"]\n      if content_disposition && content_disposition =~ /filename=\"?([^\";]+)\"?/\n        Regexp.last_match(1)\n      else\n        raise Common::InvalidResultError, \"Unable to determine filename from Content-Disposition header\"\n      end\n    end\n\n    def response_compressed?(response)\n      (response.header[\"Content-Encoding\"] && response.header[\"Content-Encoding\"] == \"gzip\") ||\n        determine_filename(response).end_with?(\".gz\")\n    end\n  end","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/page_views/fetch_result_service.rb#L26-L62","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nformat = FetchResultService.new(config, requestor_user: user).call(query_id) # raises on unexpected Content-Type\n\n# after\nresult = FetchResultService.new(config, requestor_user: user).call(query_id)\nrescue PageViews::Common::InvalidResultError => e\n  Rails.logger.warn(\"page_views result format rejected (#{e.message}), query=#{query_id}\")\n  redirect_to page_views_query_path(query_id), alert: t(\"result_unavailable\")","handlingStrategy":"try-catch","validationCode":"# guard before consuming the result\nresult = FetchResultService.new(config, requestor_user: user)\nrescue PageViews::Common::InvalidResultError\n  # fall back to re-polling status or surfacing an error\nend","typeGuard":"def supported_content_type?(response)\n  ct = response.header[\"Content-Type\"].to_s.split(\";\").first.to_s.strip\n  PageViews::Common::CONTENT_TYPE_MAPPINGS.key?(ct)\nend","tryCatchPattern":"begin\n  result = service.call(query_id)\nrescue PageViews::Common::InvalidResultError => e\n  Rails.logger.warn(\"unexpected page_views content-type: #{e.message}\")\n  render json: { error: \"result_format_unavailable\" }, status: :bad_gateway\nend","preventionTips":["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"],"tags":["http","content-type","page-views","api"],"backgroundTag":"unsupported-content-type","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}