{"record":{"id":"7959826a2dd7c640","repo":"instructure/canvas-lms","slug":"missing-content-type-header-in-response-fetch-result-service","errorCode":null,"errorMessage":"Missing Content-Type header in response.","messagePattern":"Missing Content-Type header in response\\.","errorType":"exception","errorClass":"InvalidResultError","httpStatus":null,"severity":"error","filePath":"app/services/page_views/fetch_result_service.rb","lineNumber":40,"sourceCode":"    def call(query_id)\n      uri = @configuration.uri.merge(\"/api/v5/pageviews/query/#{query_id}/results\")\n      get_with_clean_redirect(\n        uri,\n        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)","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/page_views/fetch_result_service.rb#L22-L58","documentation":"PageViews::FetchResultService#determine_result_format raises InvalidResultError when the single-result HTTP response has no Content-Type header. Like the batch variant, format selection depends on Common::CONTENT_TYPE_MAPPINGS keyed by Content-Type; without the header the body cannot be interpreted.","triggerScenarios":"The result endpoint returned a response without Content-Type — typically an empty gateway error, a dropped-header proxy response, or a raw connection failure object passed to call.","commonSituations":"Upstream outage returning malformed responses; misconfigured reverse proxy stripping Content-Type; DNS/proxy issues in dev returning non-standard responses; polling the result URL before the job finished and receiving a stub response.","solutions":["Inspect response.code and response.header before calling the service; handle non-200 explicitly","Retry with backoff — transient gateway failures often produce header-less responses","Check proxy/CDN configuration for header stripping","Verify the async query completed (poll the status endpoint) before fetching results"],"exampleFix":"// before\nresult = PageViews::FetchResultService.new(config).call(response)\n// after\nif response.header['Content-Type'].blank?\n  raise \"page views result unavailable (code=#{response.code})\"\nend\nresult = PageViews::FetchResultService.new(config).call(response)","handlingStrategy":"try-catch","validationCode":"raise 'missing Content-Type' if response.header['Content-Type'].blank?\nraise 'non-200 from page views API' unless response.code == '200'","typeGuard":null,"tryCatchPattern":"begin\n  result = PageViews::FetchResultService.new(config).call(response)\nrescue PageViews::InvalidResultError => e\n  Rails.logger.error(\"page views fetch failed: #{e.message} (code=#{response.code})\")\n  raise\nend","preventionTips":["Verify the async job finished before fetching results","Check response.code and headers before processing","Use retries with backoff for transient gateway errors","Ensure proxies preserve Content-Type headers"],"tags":["http","response-parsing","content-type"],"backgroundTag":"unexpected-response-shape","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"}