janko / down

Streaming downloads using Net::HTTP, http.rb or HTTPX

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

.open fails where .download works with HTTP Basic Authentication

brandondrew opened this issue · comments

I'm not 100% sure this is a bug, but it's certainly unexpected (to me).

The following code works fine (without Down):

    @config   = Anyway::Config.for(:campus_access_manager)
    data      = open supervisor.access_letter, http_basic_authentication: [@config['username'],@config['password']]
    send_data data.read, :type => data.content_type, :disposition => 'inline'

but I'd rather use Down, and I'd rather not save the file to disk. Switching to using Down seems fine (at least for the first step of fetching the file):

    data    = Down.download supervisor.access_letter, http_basic_authentication: [@config['username'], @config['password']]

but if I try to avoid downloading the file and passing chunks while it is being downloaded, to avoid saving it to disk, it fails with Down::ClientError - 401 Unauthorized.

    data    = Down.open supervisor.access_letter, http_basic_authentication: [@config['username'], @config['password']]
    data.each_chunk { |chunk| chunk }
    data.close

Why would HTTP Basic Auth work with download and not with open?

Is support for HTTP Basic just not implemented for open? Is this something that may be added in the near future?

Down::NetHttp#open currently only supports HTTP basic authentication via the URL, i.e. https://username:password@example.com/some/path. The reason Down::NetHttp#download supports the :http_basic_authentication option is because open-uri supports it, which #download calls internally (but not #open).

Alternatively, you can use the Down::Http backend instead, which is backed by http.rb, and where #download and #open have the same interface:

data = Down::Http.open(supervisor.access_letter) do |client|
  client.basic_auth(user: @config['username'], pass: @config['password'])
end

Note that, in order to avoid writing to disk, you should pass rewindable: false to the #open method.

@janko Awesome—thank you so much for the example code and the excellent software!

I tried using the code you provided, and I'm getting HTTP::StateError - body has already been consumed. (I had actually gotten this same error previously, and I thought I must be misunderstanding something terribly, but since it is happening with this code, I'm guessing maybe only a small tweak is needed.)

I'm using essentially the exact code you provided, with the rewindable: false added (although I get the same result without it). Here's my controller action:

  def access_letter
    @config     = Anyway::Config.for(:campus_access_manager)
    remote_file = Down::Http.open(supervisor.access_letter, rewindable: false) do |client|
      client.basic_auth(user: @config['username'], pass: @config['password'])
    end

    send_data remote_file.read, :type => remote_file.content_type, :disposition => 'inline'
    remote_file.close
  end

It's occuring in the stream! method of http 4.4.1, in lib/http/response/body.rb:

      def stream!
        raise StateError, "body has already been consumed" if @streaming == false
        @streaming = true
      end

I tried commenting out the line that raises the error (since I couldn't figure out any way that @streaming would ever not be false, and that did allow me to define remote_file but not get any data from remote_file.read. A binding.pry after defining remote_file allowed me to see a Down::ChunkedIO object and its attributes. But still no dice trying to read it. I just got back an empty string.

It's really unclear to me if this is a bug in Down, or if there's something else my code needs added to it.

(The remote_file.content_type might be an additional problem but it is separate.)

I added HTTP basic auth option to NetHttp#open in the latest Down version, I will investigate the http.rb issue.

I couldn't reproduce the issue with Down::Http on httpbin.org:

require "down/http"

io = Down::Http.open("https://httpbin.org/basic-auth/john/secret") do |client|
  client.basic_auth(user: "john", pass: "secret")
end

puts io.read
{
  "authenticated": true,
  "user": "john"
}

This can only happen if something called HTTP::Response::Body#to_s before Down::Http called #readpartial. I'm wondering what could have called it in your case 🤔