instructure/canvas-lms · error · InvalidResultError

Missing Content-Type header in response.

Error message

Missing Content-Type header in response.

What it means

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.

Solutions

  1. Inspect response.code and response.header before calling the service; handle non-200 explicitly
  2. Retry with backoff — transient gateway failures often produce header-less responses
  3. Check proxy/CDN configuration for header stripping
  4. Verify the async query completed (poll the status endpoint) before fetching results

Example fix

// before
result = PageViews::FetchResultService.new(config).call(response)
// after
if response.header['Content-Type'].blank?
  raise "page views result unavailable (code=#{response.code})"
end
result = PageViews::FetchResultService.new(config).call(response)
Defensive patterns

Strategy: try-catch

Validate before calling

raise 'missing Content-Type' if response.header['Content-Type'].blank?
raise 'non-200 from page views API' unless response.code == '200'

Try / catch

begin
  result = PageViews::FetchResultService.new(config).call(response)
rescue PageViews::InvalidResultError => e
  Rails.logger.error("page views fetch failed: #{e.message} (code=#{response.code})")
  raise
end

Prevention

When it happens

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

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

Related errors


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

Appendix: source

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

    def call(query_id)
      uri = @configuration.uri.merge("/api/v5/pageviews/query/#{query_id}/results")
      get_with_clean_redirect(
        uri,
        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)

View on GitHub (pinned to 1c9f0bb801)