instructure/canvas-lms · error · Common::InvalidResultError

Unable to determine filename from Content-Disposition header

Error message

Unable to determine filename from Content-Disposition header

What it means

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.

Solutions

  1. Inspect the raw response headers for the failing request to confirm Content-Disposition is absent vs malformed
  2. Bypass or reconfigure any proxy/CDN between Canvas and the page-views service so Content-Disposition passes through untouched
  3. Check the upstream page-views service version/release notes for changes to the results endpoint's headers
  4. As a robustness fix, add a fallback filename (e.g. "pageviews-#{query_id}.csv") when the header is missing

Example fix

// before
filename = determine_filename(response).delete_suffix(".gz")

// after
def determine_filename(response)
  content_disposition = response.header["Content-Disposition"]
  if content_disposition && content_disposition =~ /filename="?([^";]+)"?/
    Regexp.last_match(1)
  else
    "pageviews-result-#{@query_id}.csv" # fallback instead of raise
  end
end
Defensive patterns

Strategy: try-catch

Validate before calling

def has_filename?(response)
  cd = response.header["Content-Disposition"].to_s
  cd.match?(/filename="?([^";]+)"?/)
end

Type guard

def filename_from(response)
  response.header["Content-Disposition"]&.match(/filename="?([^";]+)"?/)&.captures&.first
end

Try / catch

begin
  result = service.call(query_id)
rescue PageViews::Common::InvalidResultError => e
  if e.message.include?("Content-Disposition")
    Rails.logger.warn("missing filename header for query #{query_id}")
  end
  raise
end

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/96b31c070cc89d28. Report an issue: GitHub.

Appendix: source

Thrown at app/services/page_views/fetch_result_service.rb:54

    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
  end
end

View on GitHub (pinned to 1c9f0bb801)