How to send a multipart streaming request using httpclient in ruby - ruby

I'm trying to get a multipart streaming post working with HTTPClient for Ruby. Upon which I encounter two problems.
Problem 1:
First, if I try to do a normal post via the way described in the docs; and I return it via httpbin.org
I see this happening:
Code
File.open(path_to_my_file) do |file|
body = [{ 'Content-Type' => 'application/atom+xml; charset=UTF-8',
:content => '<entry>...</entry>' },
{ 'Content-Type' => 'video/mp4',
'Content-Transfer-Encoding' => 'binary',
:content => file }]
res = #http_client.post('http://httpbin.org/post', body: body)
response = res
puts res.body
Result
{
"args": {},
"data": "",
"files": {},
"form": {
"{\"Content-Type\"=>\"application/atom+xml; charset=UTF-8\", :content=>\"<entry>...</entry>\"}": "",
"{\"Content-Type\"=>\"video/mp4\", \"Content-Transfer-Encoding\"=>\"binary\", :content=>#<File:{{path_to_file}}>}": ""
},
"headers": {
"Accept": "*/*",
"Content-Length": "322",
"Content-Type": "application/x-www-form-urlencoded",
"Date": "Mon, 20 May 2019 06:43:17 GMT",
"Host": "httpbin.org",
"User-Agent": "HTTPClient/1.0 (2.8.3, ruby 2.6.0 (2018-12-25))"
},
"json": null,
"origin": "IP, IP",
"url": "https://httpbin.org/post"
}
As you can see, I cannot see the content of the file, but only the identifier containing the URI to the File; and as such, I don't know how to fix it so that I can see and return the content. As far as I know it looks like it tries to see the File object as a String, which is of course not what I'd like it doing.
Problem2:
Whenever I create the body dynamically, that is; creating the hash-array dynamically with objects set in my code, and I try to send it asynchronously (which I believe is the correct way of getting it streaming in HTTPClient for Ruby, although I'm not entirely sure)
it sends the entire body as data, and no longer as either a form or headers.
code
request_body = []
body.files.each do |k, v|
request_body.push( { 'Content-Type' => v.content_type, :content => v.content })
end
body.values.each { |k, v| request_body << { k.to_sym => v }}
#This creates the array correctly, although I just wanted to show how it was generated
connection = #http_client.send(method + '_async', uri, body: request_body, header: headers)
response = connection.pop
# Starts reading result here
Response
"args": {},
"data": "%7B%22Content-Type%22%3D%3E%22video%2Fmp4%22%2C+%3Acontent%3D%3E%23%3CFile%3A%2Fhome%2Fuser%2Ffiles%2file.mp4%3E%7D=&%7B%3Avalue%3D%3E%22hello+world%22%7D=",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Content-Length": "247",
"Content-Type": "application/json",
"Date": "Mon, 20 May 2019 06:44:11 GMT",
"Host": "httpbin.org",
"User-Agent": "HTTPClient/1.0 (2.8.3, ruby 2.6.0 (2018-12-25))",
"ApplicationIdentifier": "identifier"
},
"json": null,
"origin": "IP, IP",
"url": "https://httpbin.org/post"
}
As you can see, it puts everything in data, and I honestly don't know how to get it to send the post with the body as a form instead of as data. I send the file here as a File object.
I have tried sending it as a normal post instead of post_async, but it does not seem to be working.
Has anyone encountered these problems before and know how I should go about fixing these? (or can you see where I went wrong so that I can at least try getting a bit further)
Thanks in advance

Regarding your first issue, it appears that the documentation is not correct. I've performed some tests, and the content type appears to be application/x-www-form-urlencoded. You need to set the content type explicitly:
body: body, header: {'Content-Type': 'multipart/form-data; boundary=ABC'}
That's not sufficient though. You also need to set the content disposition manually. For instance:
body = [{ 'Content-Type' => 'application/atom+xml; charset=UTF-8',
'Content-Disposition' => 'form-data; name="name1"',
:content => '<entry>...</entry>' },
{ 'Content-Type' => 'video/mp4',
'Content-Disposition' => 'form-data; name="file"; filename="myfile.pdf"',
'Content-Transfer-Encoding' => 'binary',
:content => file }]
With this, httpbin reports one file and one form parameter.
Your second issue is related to the same. Both your files and values need to have the content disposition set. For instance (quick and dirty, style can probably be better):
files.each do |k, v|
request_body << { 'Content-Type' => v.content_type, :content => v.content, 'Content-Disposition' => 'form-data; name="' + k + '"; filename="' + v.filename + '"' }
end
values.each { |k, v| request_body << { :content => v, 'Content-Disposition' => 'form-data; name="' + k + '"' }}
Note that you should escape any " in a name or filename.

Related

Users_not_found in HTTParty request to SlackAPI when curl works

I'm trying to use GET request using httparty gem to have info about specific user from SlackAPI. From curl it works well
curl --data "token=SLACK_TOKEN&email=user#example.com" https://slack.com/api/users.lookupByEmail
But my code below seems to be broken because I received an error {"ok":false,"error":"users_not_found"}
module Slack
class GetUserId
def call
response = HTTParty.get("https://slack.com/api/users.lookupByEmail", payload: payload, headers: headers)
end
def headers
{
'Content-Type' => 'application/json',
'Authorization' => 'Bearer SLACK_TOKEN'
}
end
def payload
{
email: "user#example.com"
}.to_json
end
end
end
If you check their documentation, it seems that API do not accept JSON-form but rather "application/x-www-form-urlencoded.
So something like:
headers: {
'Authorization' => 'Bearer SLACK_TOKEN,
"Content-Type" => "application/x-www-form-urlencoded"
},
body: {
"token=SLACK_TOKEN”,
”email=user#example.com"
...
Reference: https://api.slack.com/methods/users.lookupByEmail

HTTParty headers strange behavior

Context :
I'm using HTTParty to do a POST request on an API that accept application/json
My body is something like that (as a hash)
{:contact_id =>"12345679",
:items=> [
{:contact_id=>"123456789", :quantity=>"1", :price=>"0"},
{:contact_id=>"112315475", :quantity=>"2", :price=>"2"}
]}
Issue :
When I write the following code, this is working :
HTTParty.post('https://some-url', body: content.to_json, headers: { "Content-Type" => "application/json" } )
But when change only the => symbol in headers, to : symbol, this is not working (the API respond that some parameters are missing)
HTTParty.post('https://some-url', body: content.to_json, headers: { "Content-Type": "application/json" } )
Question :
Why changing "Content-Type" => "application/json" to "Content-Type": "application/json" lead to errors ?
I think this is something with Ruby hash that I don't understand.
Edit :
I think my question is not a duplicate of Why is this string key in a hash converted to a symbol?
It's important for HTTParty consumers, to know that HTTParty do not accept symbols for headers but only strings.
For more information, see
Content-Type Does not send when I try to use a new Hash syntax on header and
Passing headers and query params in HTTparty
Thank you #anothermh
Hash keys in Ruby can be any object type. For example, they can be strings or they can be symbols. Using a colon (:) in your hash key tells Ruby that you are using a symbol. The only way to use a string (or other object type, like Integer) for your key is to use hash rockets (=>).
When you enter { "Content-Type": "application/json" }, Ruby will convert the string "Content-Type" to the symbol :"Content-Type". You can see this yourself in the console:
{ "Content-Type": "application/json" }
=> { :"Content-Type" => "application/json" }
When you use a hash rocket it does not get converted and remains a string:
{ "Content-Type" => "application/json" }
=> { "Content-Type" => "application/json" }
HTTParty does not work with symbolized keys.

Using Gmail API to Send Attachments Larger than 10mb

I'm struggling to find a good example of the full set of requests necessary to send an email through the Gmail API containing an attachment larger than 10mb.
I've seen https://developers.google.com/gmail/api/v1/reference/users/messages/send and https://developers.google.com/gmail/api/guides/uploads#resumable, but there's nothing that ties it all together.
We're using the ruby client, but we're unable to complete this flow. With the following code, we get the following error trying to make the second request: Google::APIClient::ClientError: Recipient address required
The full body of the response is the following:
{"error"=>{"errors"=>[{"domain"=>"global", "reason"=>"invalidArgument", "message"=>"Recipient address required"}], "code"=>400, "message"=>"Recipient address required"}}
Here's the code used to generate the request:
raw = Base64.urlsafe_encode64 message_string
result1 = api_client.execute!(
:api_method => gmail_api.users.messages.to_h['gmail.users.messages.send'],
:parameters => {
:uploadType => 'resumable',
:userId => 'me'
},
:headers => {
'Content-Type' => 'application/json',
'X-Upload-Content-Type' => 'message/rfc822',
'X-Upload-Content-Length' => raw.bytesize.to_s
}
)
upload_id = result1.headers['x-guploader-uploadid']
result2 = api_client.execute!(
:api_method => gmail_api.users.messages.to_h['upload.gmail.users.messages.send'],
:body_object => {
:raw => raw
},
:parameters => {
:uploadType => 'resumable',
:upload_id => upload_id,
:userId => 'me'
},
:headers => {
'Content-Type' => message.content_type,
'Content-Length' => raw.bytesize.to_s
}
)
So the issue (thank you to #tholle) was that when sending attachments greater than 5mb and less than 35mb (but also works on messages without attachments), you do NOT base64 encode the body of the request, and use multipart as the uploadType. Unfortunately the docs don't mention this at all, and the error messages don't indicate that either.
Here's a working example that was able to send a 20mb attachment. Hopefully this will help anyone else who has wasted countless hours trying to figure this one out!
result = api_client.execute!(
:api_method => gmail_api.users.messages.to_h['gmail.users.messages.send'],
:body => rfc822_message_string,
:parameters => {
:uploadType => 'multipart',
:userId => 'me'
},
:headers => {
'Content-Type' => 'message/rfc822',
}
)
I am working on a JavaScript client and I finally found a way to send email using resumable method. (Thank you #Tholle and #jwg2s). Although this is in JavaScript client it should work about the same in other client too.
This is what I did:
// MIME Mail Message data. Copied form above #Tholle 's message.
let mail = [
'Content-Type: multipart/mixed; boundary="foo_bar_baz"\r\n',
"MIME-Version: 1.0\r\n",
"to: to#gmail.com\r\n",
"from: from#gmail.com\r\n",
"subject: i am subject\r\n\r\n",
"--foo_bar_baz\r\n",
'Content-Type: text/plain; charset="UTF-8"\r\n',
"MIME-Version: 1.0\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"The actual message text goes here\r\n",
"--foo_bar_baz\r\n",
"Content-Type: application/json; name=package.json\r\n",
"Content-Transfer-Encoding: base64\r\n",
"Content-Disposition: attachment; filename=package.json\r\n\r\n",
"<base64 file data. data according to content type>",
"\r\n",
"--foo_bar_baz--",
].join("");
// get resumable upload link.
let resumableURL = "";
gapi.client
.request({
path: "/upload/gmail/v1/users/me/messages/send?uploadType=resumable",
headers: {
"X-Upload-Content-Type": "message/rfc822",
},
method: "post",
})
.then(
(res) => {
resumableURL = res.headers.location;
console.log(res);
},
(err) => console.log(err)
);
// send email
gapi.client
.request({
path: resumableURL,
headers: {
"Content-Type": "message/rfc822",
},
method: "post",
body: mail,
})
.then(
(res) => {
console.log(res);
},
(err) => console.log(err)
);
To convert gapi.client.request to Fetch API call you just need to add Authorization: Bearer <access_token> to header field. I have tried using Fetch API but response were blocked due to cors error so api client like Postman should be used.
To do more with resumable upload method check documentation: Upload Attachment

Header parameter missing when making POST request using Rest-Client Ruby Gem

Hi I am successfully able to post from Post man , but unable to do so from Ruby Rest client.
Post details
Post Man Request
POST /endpoint HTTP/1.1
Host: host:11400
Accept: application/json
HTTP_USER: userid
fname: fname
lname: lname
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 589a5345-e384-bd71-d690-60987165487b
{ "rtdt":"09/08/2016","jobs":[{"pid":53} , {"pid":54}]}
the rest code I ve tried multiple ways including below
method tried
p RestClient.post 'http://url_details',
Accept: 'application/json',
'HTTP_USER'.to_sym => 'userid',
fname: 'name',
lname: '',
'Content-Type'.to_sym => 'application/json',
payload: JSON.parse('{ "rtdt":"09/08/2016","jobs":[{"pid":53} , {"pid":54}]}')
method tried
p RestClient.post 'http://url_details/job', http_user: 'userid', content_type: :json, accept: :json
method tried
p #uber_ride = (RestClient::Request.execute(
:method => :post,
:url => 'http://url_details/job',
'HTTP_USER' => 'userid',
:headers => {:content_type => 'application/json', :accept => 'application/json', :HTTP_USER => 'userid', :fname => 'name', :lname => 'lname'}
))
method tried
p '++++++++++++++++++++++++++++++++'
p RestClient.post($Current_Api_Endpoint,
$Current_Payload,
:http_user =>'userid',
headers:
{:accept => 'application/json',
:content_type => 'application/json',
:http_user =>'userid',
:fname => 'name',
:lname => 'lname'}
)
p '++++++++++++++++++++++++++++++++'
method tried
p env1 = ENV['http_proxy']
# p a = {:method => :post, :url => 'http://url_details/job', :headers => { :accept => 'application/json', :content_type => 'application/json', :http_user => 'userid', :fname => 'name', :lname => 'lname' }, :payload => { :rtdt => "06/10/2016",:jobs => [{:pid => 53} , {:pid => 54}]} }
p a = {
method: 'post',
url: 'http://url_details/job',
proxy: ENV['http_proxy'],
headers: {accept: 'application/json', content_type: 'application/json', http_user: 'userid', fname: 'name', lname: 'lname'},
payload: { rtdt: '06/10/2016', jobs: {pid: 53}}
}
I am getting error
HTTP Status 500 - HTTP_USER header not found in request
torg.springframework.security.web.authentication
I Found out the issue since then. All headers are able to be sent, except this one - This one in CAPS. rest of them in small characters. How to solve it
Ruby's Net::HTTP implementation does not transmit ALL_CAPS_UNDERSCORED header names. RestClient relies on Net::HTTP. So, for example, the HTTP_USER header is actually transformed into Http_user when transmitted.
Alertnative: curb
Curl, however, does not have this limitation. It transmits header names verbatim. Therefore instead of using RestClient, you could use the curb gem, which is a wrapper around libcurl. This seems to work:
require "curb"
Curl::Easy.http_post("http://URL_REDACTED") do |http|
http.headers["Accept"] = "application/json"
http.headers["Content-Type"] = "application/json"
http.headers["HTTP_USER"] = "userid"
http.post_body = '{ "rtdt":"09/08/2016","jobs":[{"pid":53} , {"pid":54}]}'
end
Alternative: excon
The excon gem is another possibility. It implements HTTP in pure Ruby, without relying on Net::HTTP. It does its own header processing and allows ALL_CAPS headers. So this should work:
require "excon"
Excon.post(
"http://URL_REDACTED",
:headers => {
"Accept" => "application/json",
"Content-Type" => "application/json",
"HTTP_USER" => "userid"
},
:body => '{ "rtdt":"09/08/2016","jobs":[{"pid":53} , {"pid":54}]}'
)
Anyway, I would argue that the server you are trying to connect to is ultimately at fault here, because headers are supposed to be evaluated as case-insensitive: Are HTTP headers case-sensitive?. If the server requires a header be in all uppercase, that is a bug.
The RestClient README examples indicate that the correct usage is:
RestClient.post(url, payload, headers)
So all you have to do is:
RestClient.post(
"http://URL_REDACTED",
'{ "rtdt":"09/08/2016","jobs":[{"pid":53} , {"pid":54}]}',
content_type: :json,
accept: :json,
"HTTP_USER" => "userid"
)
I tested against http://requestb.in and got the expected headers and body:
HEADERS
Total-Route-Time: 0
Connection: close
Via: 1.1 vegur
Connect-Time: 0
Http-User: userid
Content-Type: application/json
Content-Length: 55
User-Agent: rest-client/2.0.0 (darwin15.4.0 x86_64) ruby/2.3.1p112
X-Request-Id: c7cd819f-8901-41b9-8cbb-3b6d1895cd67
Accept-Encoding: gzip
Host: requestb.in
Accept: application/json
RAW BODY
{ "rtdt":"09/08/2016","jobs":[{"pid":53} , {"pid":54}]}

restClient as Post method with cookies

I am working in Ruby and I am new to this completely.
I need to make restclient post request along with my cookies and also with the headers.
I already had response from my first get call and I am getting the hidden value from that response, then I tried to make post request with previously generated headers and cookies.
I tried with the following code
require 'rest_client'
res = RestClient::Request.execute(
method: :post,
url: 'https://evision-sperf.as.itc.ca/urd/sit?q=&label=Search+UBC&site=*.ubc.ca&%25.WEB_HEAD.MENSYS.1=&%25.DUM_MESSAGE.MENSYS.1=&SCREEN_WIDTH.DUMMY.MENSYS.1=1280&SCREEN_HEIGHT.DUMMY.MENSYS.1=720&%25.DUMMY.MENSYS.1=2015112407571924%1BFC6A057FA9E603F56F47993BD4B50175B30B66E67F252E2BEE51F0B324DF2743AAF2D3373D219887287FFD0EFC1ECE4BD122EDB3AA44D7E7889464863851C9EB%1B0A8A488A92C411E5A4FEF2C9F2BFA664%1BNONE&RUNTIME.DUMMY.MENSYS.1=&PARS.DUMMY.MENSYS.1=&MUA_CODE.DUMMY.MENSYS.1=800564&PASSWORD.DUMMY.MENSYS.1=Ubc123%24&BP101.DUMMY_B.MENSYS.1=Login+%3E%3E',
headers: {"JSESSIONID"=>"F61C6D1266F4668E7E004A8D0F835C03", "ace-sis-perf-svapps"=>"R2440364609"},
cookies: {'JSESSIONID' => 'F61C6D1266F4668E7E004A8D0F835C03', 'ace-sis-perf-svapps' => 'R2440364609'}
)
puts res.code
puts res.to_str
But the response is not right, It says error.
Can any one assist here on how to make this post call with headers and cookies?
Try this:
result = RestClient::Request.execute(
method: :post,
url: 'http://localhost/example.json',
headers: {'X-HEADER' => 'header-value'},
cookies: {'cookie-name' => 'value'}
)
More details here: https://github.com/rest-client/rest-client#passing-advanced-options
You can also use https://httpbin.org/ to test your code. For example:
require 'rest_client'
res = RestClient::Request.execute(
method: :post,
url: 'https://httpbin.org/post',
headers: {"X-HEADER-1"=>"header-value-1", "X-HEADER-2"=>"header-value-2"},
cookies: {'MY-COOKIE-1' => 'cookie-1-value', 'MY-COOKIE-2' => 'cookie-2-value'}
)
puts res.code
puts res.to_str
Will return you such:
200
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*; q=0.5, application/xml",
"Accept-Encoding": "gzip, deflate",
"Cookie": "MY-COOKIE-1=cookie-1-value;MY-COOKIE-2=cookie-2-value",
"Host": "httpbin.org",
"User-Agent": "Ruby",
"X-Header-1": "header-value-1",
"X-Header-2": "header-value-2"
},
"json": null,
"origin": "200.100.100.100",
"url": "https://httpbin.org/post"
}
As you can see, the code sets headers and cookies correctly.

Resources