antiwork/gumroad · error · ActiveStorage::FileNotFoundError

We couldn't download that file, please check the URL and try

Error message

We couldn't download that file, please check the URL and try again.

What it means

Raised as ActiveStorage::FileNotFoundError at line 201 when SsrfFilter.get completes but the response is not a Net::HTTPSuccess, and mapped to this message; #process's rescue also maps INTERNET_EXCEPTIONS (SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, ...) to the same string. It means the URL was structurally valid and SSRF-safe but the fetch itself failed: a 4xx/5xx status or a transport-level error. (Note: an unresolvable hostname maps to the 'valid public URL' message instead, via SsrfFilter::UnresolvedHostname.)

Source

Thrown at app/services/create_public_media_service.rb:201

      raise URI::InvalidURIError, "URL '#{normalized_url}' is not a web url" unless uri.scheme.in?(%w[http https])
      raise URI::InvalidURIError, "URL must include a valid host" if uri.host.blank?

      tempfile = Tempfile.new(binmode: true)
      begin
        response = SsrfFilter.get(normalized_url) do |http_response|
          raise RemoteFileTooLarge if http_response["content-length"].to_i > MAX_IMAGE_BYTES

          write_file = http_response.is_a?(Net::HTTPSuccess)
          received_bytes = 0
          byte_limit = MAX_IMAGE_BYTES
          http_response.read_body do |chunk|
            received_bytes += chunk.bytesize
            raise RemoteFileTooLarge if received_bytes > byte_limit

            tempfile.write(chunk) if write_file
          end
        end
        raise ActiveStorage::FileNotFoundError unless response.is_a?(Net::HTTPSuccess)

        tempfile.rewind
        # Sniff the real content type from the file bytes. The remote server's header is used only
        # as a hint — a mislabeled or disguised file is classified by what it actually contains.
        content_type = Marcel::MimeType.for(tempfile, name: filename_from(uri), declared_type: response.content_type)
        tempfile.rewind
        ActiveStorage::Blob.create_and_upload!(
          io: tempfile,
          filename: filename_with_extension(filename_from(uri), content_type),
          content_type:,
        )
      ensure
        tempfile.close!
      end
    end

    def normalize_url(raw)
      value = raw.to_s

View on GitHub (pinned to afeacbd394)

Solutions

  1. Open the URL in an incognito window to confirm it is publicly reachable without cookies
  2. Use a stable permalink (your own hosting) rather than expiring share links
  3. Retry after a short wait if the failure looked like a transient 5xx or connection reset
  4. Direct-upload the file (signed_blob_id) so no remote fetch is needed at all

Example fix

# before
url: 'https://s3.amazonaws.com/bucket/logo?X-Amz-Expires=60' # link expired -> 403
# => failure: We couldn't download that file...

# after
url: 'https://seller-site.com/assets/logo.png' # permanent, public
Defensive patterns

Strategy: retry

Validate before calling

require 'net/http'

uri = URI(url)
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5) do |http|
  code = http.head(uri.request_uri).code
  raise "endpoint returns #{code}" unless code.start_with?('2')
end

Try / catch

result = nil
begin
  attempts = (attempts || 0) + 1
  result = CreatePublicMediaService.new(seller:, url:).process
  # HTTP 5xx / connection resets land here as failure Result, not exceptions:
  # retry the whole process call with backoff while attempts < 3
end while !result.success? && result.error_message.include?(%q[couldn't download]) && attempts < 3

Prevention

When it happens

Trigger: The remote server returns 404 (file moved/deleted), 403 (hotlink protection or an expired presigned URL), or 5xx; or the TCP connection is refused/reset mid-fetch. URL passed all scheme/host/SSRF checks, so the failure is downstream of them.

Common situations: Expired S3/Google-Drive presigned links, hosts that block non-browser user-agents or referers, origin outages behind a CDN, transient 502/503s.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/dc5b69adf25e8240. Report an issue: GitHub.