{"record":{"id":"96b31c070cc89d28","repo":"instructure/canvas-lms","slug":"unable-to-determine-filename-from-content-disposition-header-96b31c","errorCode":null,"errorMessage":"Unable to determine filename from Content-Disposition header","messagePattern":"Unable to determine filename from Content-Disposition header","errorType":"exception","errorClass":"Common::InvalidResultError","httpStatus":null,"severity":"error","filePath":"app/services/page_views/fetch_result_service.rb","lineNumber":54,"sourceCode":"\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\nend\n","sourceCodeStart":36,"sourceCodeEnd":64,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/page_views/fetch_result_service.rb#L36-L64","documentation":"PageViews::FetchResultService#determine_filename raises Common::InvalidResultError when the Content-Disposition header is missing or contains no filename= attribute matching /filename=?([^\";]+)\"?/. The service derives the download filename (and gzip detection via the .gz suffix) from this header, so without it a DownloadableResult cannot be built.","triggerScenarios":"Calling FetchResultService#call(query_id) (directly or via response_compressed?) when the results response has no Content-Disposition header, or one without a filename parameter (e.g. 'inline', 'attachment' with no filename, or a header stripped by an intermediate proxy/CDN).","commonSituations":"A reverse proxy or CDN (e.g. CloudFront, nginx) strips or rewrites Content-Disposition; the upstream service was updated and no longer sets filename on the results endpoint; the request actually hit an error/interstitial page rather than the file download.","solutions":["Inspect the raw response headers for the failing request to confirm Content-Disposition is absent vs malformed","Bypass or reconfigure any proxy/CDN between Canvas and the page-views service so Content-Disposition passes through untouched","Check the upstream page-views service version/release notes for changes to the results endpoint's headers","As a robustness fix, add a fallback filename (e.g. \"pageviews-#{query_id}.csv\") when the header is missing"],"exampleFix":"// before\nfilename = determine_filename(response).delete_suffix(\".gz\")\n\n// after\ndef 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    \"pageviews-result-#{@query_id}.csv\" # fallback instead of raise\n  end\nend","handlingStrategy":"try-catch","validationCode":"def has_filename?(response)\n  cd = response.header[\"Content-Disposition\"].to_s\n  cd.match?(/filename=\"?([^\";]+)\"?/)\nend","typeGuard":"def filename_from(response)\n  response.header[\"Content-Disposition\"]&.match(/filename=\"?([^\";]+)\"?/)&.captures&.first\nend","tryCatchPattern":"begin\n  result = service.call(query_id)\nrescue PageViews::Common::InvalidResultError => e\n  if e.message.include?(\"Content-Disposition\")\n    Rails.logger.warn(\"missing filename header for query #{query_id}\")\n  end\n  raise\nend","preventionTips":["Configure proxies/CDNs to pass Content-Disposition through unchanged","Verify the upstream results endpoint still sets filename after service upgrades","Add a fallback filename derived from query_id if you control the client code","Curl the endpoint and inspect headers when filename errors appear"],"tags":["http","headers","content-disposition","page-views"],"backgroundTag":"missing-response-header","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"}