lostisland/sawyer

Use sawyer with multipart POST?

ldonnet opened this issue · 7 comments

Hi, I use your great gem to make an API client ruby. It works well when I make basic requests but I have one probleme when I use multipart post. It seems that body isn't parsed by multipart-post.

When I try this with Faraday it works like a charm :

conn = Faraday.new(:url => "http://localhost:8080/chouette_iev/") do |conn|
          # POST/PUT params encoders:
        conn.request :multipart
        conn.request :url_encoded

        conn.adapter :net_http
    end

    action_params_io = Faraday::UploadIO.new("/home/luc/parameters_import_neptune.json", "application/json", "parameters.json")    

    transport_data_io = Faraday::UploadIO.new("/home/luc/demo/15568799.xml", "application/xml", "15568799.xml")

    payload = {
                                                  :file1 => action_params_io,
                                                  :file2 => transport_data_io,
                        }                       

    conn.post "referentials/test2/importer/neptune", payload

But when I user this sawyer configuration body is not splitted in parts :

      opts = {
        :links_parser => Sawyer::LinkParsers::Hal.new
      }
      conn_opts = {
          :headers => {
            :accept => "multipart/form-data",
            :user_agent => " Ievkit Ruby Gem 0.1.0"
          }
        }
      conn_opts[:builder] =  Faraday::RackBuilder.new do |builder|                  
      builder.use Faraday::Request::Multipart
      builder.use Faraday::Request::UrlEncoded
      builder.use Ievkit::Response::RaiseError
      builder.use FaradayMiddleware::FollowRedirects
      builder.use Faraday::Response::Logger

      builder.adapter Faraday.default_adapter
    end
      conn_opts[:proxy] = {}
      opts[:faraday] = Faraday.new(conn_opts)
      opts

@agent ||= Sawyer::Agent.new(api_endpoint, sawyer_options) do |http|
        http.headers[:accept] = default_media_type
        http.headers[:content_type] = "application/json"
        http.headers[:user_agent] = user_agent

Is it possible to make a multipart-post request? Is there a specific way to call?

If I comment the encoding of the body multipart works again :

    # Makes a request through Faraday.
    #
    # method  - The Symbol name of an HTTP method.
    # url     - The String URL to access.  This can be relative to the Agent's
    #           endpoint.
    # data    - The Optional Hash or Resource body to be sent.  :get or :head
    #           requests can have no body, so this can be the options Hash
    #           instead.
    # options - Hash of option to configure the API request.
    #           :headers - Hash of API headers to set.
    #           :query   - Hash of URL query params to set.
    #
    # Returns a Sawyer::Response.
    def call(method, url, data = nil, options = nil)
      if NO_BODY.include?(method)
        options ||= data
        data      = nil
      end

      options ||= {}
      url = expand_url(url, options[:uri])
      started = nil
      res = @conn.send method, url do |req|
        if data
          req.body = data #.is_a?(String) ? data : encode_body(data)           <= Comment here
        end
        if params = options[:query]
          req.params.update params
        end
        if headers = options[:headers]
          req.headers.update headers
        end
        started = Time.now
      end

      Response.new self, res, :sawyer_started => started, :sawyer_ended => Time.now
    end

I think I could have the right behaviour with an updated serializer. Do you have a specific one for multipart request?

Thanks
Luc Donnet

Good find @ldonnet. This line is more of a convenience to encode Ruby objects before making the request. What's data at this point in your case? A Faraday::UploadIO object?

data is an hash of Faraday::UploadIO object as I describe before :

action_params_io = Faraday::UploadIO.new("/home/luc/parameters_import_neptune.json", "application/json", "parameters.json")    

    transport_data_io = Faraday::UploadIO.new("/home/luc/demo/15568799.xml", "application/xml", "15568799.xml")

    payload = {
                                                  :file1 => action_params_io,
                                                  :file2 => transport_data_io,
                        }   

Do you have any idea how to make this feature works?
I could make a fix but if you have a beginning of idea it could be useful.

I'm not very proud of this solution but it should be a fix for me. I create my own serializer and create fake class :

def self.multipart
      new(IevMultipart)
    rescue LoadError
    end

    class IevMultipart
      def self.dump(data)
        data
      end

      def self.load(data)
        data
      end
    end

But I need now to have 2 sawyer configurations : one for json request and one for multipart.

@ldonnet I think a custom Serializer is the way to go. Could you get by with a single configuration if your custom serializer knows how to handle Faraday::UploadIO as a special case?

But I need now to have 2 sawyer configurations : one for json request and one for multipart.

Oof. In a better world, that #encode_body would take some other request details so the serializer can make informed decisions about how to encode that body. You may be able to get by with a single serializer that checks for any Faraday::UploadIO objects in the data values to decide whether to encode as json or let Faraday handle it.