Faraday: How to customize Content-Disposition for the file in a multipart request?

Created on 7 Jun 2012  路  6Comments  路  Source: lostisland/faraday

I tried to use Faraday to connect to the DocuSign API which requires a multipart post including a PDF to be signed. However, I was unable to get the request built up as their API required and had to drop down the Net::HTTP to get it done. I could see sections of the multipart-post gem that looked like what I would need to tap into (The FilePart class in the Parts module) but Faraday didn't seem to be using that portion of the gem code when building up the request (at least my pry breakpoint never got hit inside that method).

This is my working Net:HTTP post request... is this possible with Faraday? Of particular note is the documentid parameter that they require in the file's Content-Disposition. Passwords/keys changed for security reasons.

require "net/http"
require "uri"

BOUNDARY = "myboundary"

uri = URI.parse("https://demo.docusign.net/restapi/v1/accounts/123456789/envelopes")
file = "test.pdf"

post_body = ""
post_body << "\r\n"
post_body << "--#{BOUNDARY}\r\n"
post_body << "Content-Type: application/json\r\n"
post_body << "Content-Disposition: form-data\r\n"
post_body << "\r\n"
post_body << "{
  \"emailBlurb\": \"eblurb\",
  \"emailSubject\": \"esubj\",
  \"documents\": [
    {
      \"documentId\": \"1\",
      \"name\": \"#{File.basename(file)}\"
    }
  ],
  \"recipients\": {
    \"signers\": [
      {
        \"email\": \"[email protected]\",
        \"name\": \"FunGuy\",
        \"recipientId\": \"1\"
      }
    ]
  },
  \"status\": \"sent\"
}"
post_body << "\r\n"
post_body << "--#{BOUNDARY}\r\n"
post_body << "Content-Type: application/pdf\r\n"
post_body << "Content-Disposition: file; filename=\"#{File.basename(file)}\"; documentid=1\r\n"
post_body << "\r\n"
post_body << File.read(file)
post_body << "\r\n"
post_body << "--#{BOUNDARY}--\r\n"

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

headers = {
  "X-DocuSign-Authentication" => "<DocuSignCredentials><Username>[email protected]</Username><Password>redacted</Password><IntegratorKey>COMP-129fjghs-cb34-4ab6-87ec-59db47d34b32</IntegratorKey></DocuSignCredentials>",
  "Content-Type" => "multipart/form-data, boundary=#{BOUNDARY}",
  "Accept" => "application/json",
  "Content-Length" => "#{post_body.length}"
}

request = Net::HTTP::Post.new(uri.request_uri, headers)
request.body = post_body

response = http.request(request)

puts response.body

This was the Faraday code I was trying to use:

require 'faraday'

conn = Faraday.new(:url => 'https://demo.docusign.net/restapi/v1/',
  :headers => {
    "X-DocuSign-Authentication" => "<DocuSignCredentials><Username>[email protected]</Username><Password>redacted</Password><IntegratorKey>COMP-129fjghs-cb34-4ab6-87ec-59db47d34b32</IntegratorKey></DocuSignCredentials>",
    "Accept" => "application/json",
    "Content-Type" => "application/json"
  }) do |builder|
    builder.request  :multipart
    builder.adapter  :net_http
end

payload = { :file => Faraday::UploadIO.new('test.pdf','application/pdf') }

response = conn.post("accounts/123456789/envelopes", payload) do |req|
  req.body = "{
  \"emailBlurb\": \"eblurb\",
  \"emailSubject\": \"esubj\",
  \"documents\": [
    {
      \"documentId\": \"1\",
      \"name\": \"#{payload[:file].original_filename}\"
    }
  ],
  \"recipients\": {
    \"signers\": [
      {
        \"email\": \"[email protected]\",
        \"name\": \"FunGuy\",
        \"recipientId\": \"1\"
      }
    ]
  },
  \"status\": \"sent\"
}
"
end

puts response.body

The error I received from their API was that no documents were attached, presumably because I was unable to specify the documentid as required.

Any help/guidance would be greatly appreciated!

Thanks,
-Jon

feature help wanted

Most helpful comment

The multipart-post gem doesn't really give you a place to set this in a safe way. However, the solution with Faraday::UploadIO should work just fine.

payload = { :file => Faraday::UploadIO.new('test.pdf', "application/pdf", 'test.pdf', "Content-Disposition" => "file; documentId=1") }

That Content-Disposition value gets thrown in, untouched, right here. Here's a sample dumped to STDOUT by a simple tweak of the current tests.

-------------RubyMultipartPost-d4d88346bd5fabb730c3fe0de769af9f
Content-Disposition: file; documentId=1; name="b[c]"; filename="multipart_spec.rb"
Content-Length: 2231
Content-Type: text/x-ruby
Content-Transfer-Encoding: binary

# frozen_string_literal: true
...

Here's the diff I used:

diff --git a/lib/faraday/upload_io.rb b/lib/faraday/upload_io.rb
index e16fd7a..8db4cfa 100644
--- a/lib/faraday/upload_io.rb
+++ b/lib/faraday/upload_io.rb
@@ -42,6 +42,7 @@ module Faraday
         if (result = io.read(length))
           got_result ||= !result.nil?
           result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
+          puts "MULTIPART: #{result}"
           outbuf << result
           length -= result.length if length
           break if length&.zero?
diff --git a/spec/faraday/request/multipart_spec.rb b/spec/faraday/request/multipart_spec.rb
index 809d9e0..1e48ee8 100644
--- a/spec/faraday/request/multipart_spec.rb
+++ b/spec/faraday/request/multipart_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Faraday::Request::Multipart do
       b.adapter :test do |stub|
         stub.post('/echo') do |env|
           posted_as = env[:request_headers]['Content-Type']
+          puts env[:body].read
           [200, { 'Content-Type' => posted_as }, env[:body]]
         end
       end
@@ -54,7 +55,7 @@ RSpec.describe Faraday::Request::Multipart do
       {
         a: 1,
         b: {
-          c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby'),
+          c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby', nil, 'Content-Disposition' => 'file; documentId=1'),
           d: 2
         }
       }

Closing this, as there's no real bug other than the poor way the multipart-post gem was shoehorned in without documentation. I'm going to do the following:

  • Add test to show this working.
  • Document this in readme for Faraday v1.0 release #950
  • Rethink multipart api in Faraday for Faraday v2 #953

All 6 comments

Hey, this does seem like something you can't do with Faraday at the moment. Keep using your workaround, but I'll look into how we can enable this, and update this ticket.

Awesome, thanks Mislav!

Upon further investigation it seems that I can add some options to the UploadIO

payload = { :file => Faraday::UploadIO.new('test.pdf', "application/pdf", 'test.pdf', "Content-Disposition" => "file; documentId=1") }

Which if the FileParts class of the multipart-gem gets hit should let me inject that documentid into the Content-Disposition. However, it doesn't seem like that portion of the multipart-post gem is getting invoked on my local system even though I swear it should be.

What am I missing? Why isn't Faraday utilizing that portion of the multipart-post gem? It's like it's not even detecting that it's a multipart post... can you not have a custom body and a payload at the same time?

Anyone have any luck with this?

As it was already pointed out, this is currently not managed by the multipart middleware.
You can see the middleware here.
I'm not personally an expert on Multipart requests so appreciate if anyone wants to jump in to add support for this 馃槃

The multipart-post gem doesn't really give you a place to set this in a safe way. However, the solution with Faraday::UploadIO should work just fine.

payload = { :file => Faraday::UploadIO.new('test.pdf', "application/pdf", 'test.pdf', "Content-Disposition" => "file; documentId=1") }

That Content-Disposition value gets thrown in, untouched, right here. Here's a sample dumped to STDOUT by a simple tweak of the current tests.

-------------RubyMultipartPost-d4d88346bd5fabb730c3fe0de769af9f
Content-Disposition: file; documentId=1; name="b[c]"; filename="multipart_spec.rb"
Content-Length: 2231
Content-Type: text/x-ruby
Content-Transfer-Encoding: binary

# frozen_string_literal: true
...

Here's the diff I used:

diff --git a/lib/faraday/upload_io.rb b/lib/faraday/upload_io.rb
index e16fd7a..8db4cfa 100644
--- a/lib/faraday/upload_io.rb
+++ b/lib/faraday/upload_io.rb
@@ -42,6 +42,7 @@ module Faraday
         if (result = io.read(length))
           got_result ||= !result.nil?
           result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
+          puts "MULTIPART: #{result}"
           outbuf << result
           length -= result.length if length
           break if length&.zero?
diff --git a/spec/faraday/request/multipart_spec.rb b/spec/faraday/request/multipart_spec.rb
index 809d9e0..1e48ee8 100644
--- a/spec/faraday/request/multipart_spec.rb
+++ b/spec/faraday/request/multipart_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Faraday::Request::Multipart do
       b.adapter :test do |stub|
         stub.post('/echo') do |env|
           posted_as = env[:request_headers]['Content-Type']
+          puts env[:body].read
           [200, { 'Content-Type' => posted_as }, env[:body]]
         end
       end
@@ -54,7 +55,7 @@ RSpec.describe Faraday::Request::Multipart do
       {
         a: 1,
         b: {
-          c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby'),
+          c: Faraday::UploadIO.new(__FILE__, 'text/x-ruby', nil, 'Content-Disposition' => 'file; documentId=1'),
           d: 2
         }
       }

Closing this, as there's no real bug other than the poor way the multipart-post gem was shoehorned in without documentation. I'm going to do the following:

  • Add test to show this working.
  • Document this in readme for Faraday v1.0 release #950
  • Rethink multipart api in Faraday for Faraday v2 #953
Was this page helpful?
0 / 5 - 0 ratings

Related issues

yusefu picture yusefu  路  3Comments

JasonBarnabe picture JasonBarnabe  路  4Comments

mattmill30 picture mattmill30  路  4Comments

aleksb86 picture aleksb86  路  3Comments

jordansissel picture jordansissel  路  5Comments