Using RCurl postForm to gather JSON data - r

I am trying to extract time series data from the BLS API using RCurl.
The BLS supplies the following sample code for command line extraction:
curl -i -X POST -H 'Content-Type: application/json'
-d '{"seriesid":["LEU0254555900", "APU0000701111"],
"startyear":"2002", "endyear":"2012"}'
http://api.bls.gov/publicAPI/v1/timeseries/data/
I have also confirmed that the specified files (i.e. series ids) are both present as the following both yield a JSON formatted object:
require(RCurl)
bls.content_test1 <- getURLContent("http://api.bls.gov/publicAPI/v1/timeseries/data/LEU0254555900")
bls.content_test2 <- getURLContent("http://api.bls.gov/publicAPI/v1/timeseries/data/APU0000701111")
Based on a variety of posts with the RCurl tag (and this post in particular), I have ported the command line script to the following chunk of code:
require(RJSONIO)
jsonbody <- toJSON(list("seriesid"=paste('"["CFU0000008000"', '[LEU0254555900"]"')
,"startyear"="2012"
,"endyear"="2013"))
httpheader <- c(Accept="application/json; charset=UTF-8",
"Content-Type"="application/json")
bls.content <- postForm("http://api.bls.gov/publicAPI/v1/timeseries/data/"
,.opts=list(httpheader=httpheader
,postfields=jsonbody))
Which yields:
[1] "{\"status\":\"REQUEST_FAILED\",\"responseTime\":0,\"message\":[\"Your request has failed, please check your input parameters and try your request again.\"],\"Results\":null}"
attr(,"Content-Type")
charset
"application/json" "UTF-8"
Does this appear to be a problem with my implementation of RCurl or does this appear to be a problem with the BLS API?

It's actually a problem with the way you are creating your json body. With your version if you do cat(jsonbody) you get
{
"seriesid": "\"[\"CFU0000008000\" [LEU0254555900\"]\"",
"startyear": "2012",
"endyear": "2013"
}
which has those extra escapes and brackets in there. It's not correct. Instead try
jsonbody <- toJSON(list("seriesid"=c("CFU0000008000", "LEU0254555900"),
startyear="2012",
endyear="2013"))
which gives
{
"seriesid": [ "CFU0000008000", "LEU0254555900" ],
"startyear": "2012",
"endyear": "2013"
}
which is valid JSON. Just changing that part and using the rest of the code as you had it gives me a REQUEST_SUCCEEDED message.

Here's an approach with httr:
library(httr)
library(jsonlite)
# Need unbox() to tell jsonlite() numbers are scalars, not vectors of length 1
body <- list(
seriesid = c("CFU0000008000", "LEU0254555900"),
startyear = unbox(2012),
endyear = unbox(2013)
)
r <- POST("http://api.bls.gov/publicAPI/v1/timeseries/data/", body = body, encode = "json")
stop_for_status(r)
# Need to specify type since site returns incorrect type of text/plain
str(content(r, type = "application/json"))

Related

httr POST request error: upstream connect error or disconnect/reset before headers. reset reason: connection termination

I am currently trying to upload a CSV file to a proprietary service via httr::POST(). Unfortunately the Admins are not experienced in R and can give only little support.
This is an example on how it should look like in the command line:
curl -X POST --header 'Content-Type: multipart/form-data' \
-F file=#"member-1-test.csv.gz" 'https://some/api/endpoint'
So, in the following code I just try to stick with the example (and additionally provide a token).
> library(tidyverse)
> library(httr)
# Provide some test data with characters specifically quoted
> test_file <- tibble::tribble(
~keytype, ~key, ~action, ~segment,
6,"\"https://www.google.com\"", 0, 37372818,
6,"\"https://www.sport1.de\"" , 0, 37372818
)
> data.table::fwrite(test_file, "test.csv", quote = FALSE)
> file <- upload_file(path = "C:/R/projects/DefaultWorkingDirectory/test.csv")
> res <- POST(
url = "https://some/api/endpoint",
body = list(file = file),
add_headers(.headers = c('Content-Type' = "multipart/form-data", "Authorization" = token))
)
This gives me the follwing error:
> res
Response [https://some/api/endpoint]
Date: 2020-11-18 09:35
Status: 503
Content-Type: text/plain
Size: 95 B
> content(res, encoding = "UTF8")
"upstream connect error or disconnect/reset before headers. reset reason: connection termination"
Any help or guidance on how to move forward with this issue is very much appreciated. Thanks!
Directly after posting the question it was already solved by one of the Admins :)
The issue was that the encoding needs to be set to "multipart" and that a specific content type needs to be provided which is similar to JSON but needs to be added to the "Accept"-header field.
Here the answer for any future references:
> res <- POST(
url = "https://some/api/endpoint",
body = list(
file = upload_file("test.csv")
),
add_headers(c(
"Authorization" = token,
"Accept" = "specific_format_for_the_application"
)),
encode = "multipart",
verbose()
)

How to send curl variables to plumber function dynamically?

I want to dynamically call the plumber API based on any number of input variables. I need to map the curl input to the input of a function name. For example if the function has an input hi then, curl -s --data 'hi=2' means that hi=2 should be passed as an input parameter to the function. This can be done directly in R with match.call() but it is failing while calling it through the plumber API.
Take the function
#' #post /API
#' #serializer unboxedJSON
tmp <- function(hi) {
out <- list(hi=hi)
out <- toJSON(out, pretty = TRUE, auto_unbox = TRUE)
return(out)
}
tmp(hi=2)
out: {hi:2}
Then
curl -s --data 'hi=10' http://127.0.0.1/8081/API
out: {\n \"hi\": \"2\"\n}
Everything looks good. However, take the function
#' #post /API
#' #serializer unboxedJSON
tmp <- function(...) {
out <- match.call() %>%
as.list() %>%
.[2:length(.)] # %>%
out <- toJSON(out, pretty = TRUE, auto_unbox = TRUE)
return(out)
}
tmp(hi=2)
out: {hi:2}
Then
curl -s --data 'hi=10' http://127.0.0.1/8081/API
out: {"error":"500 - Internal server error","message":"Error: No method asJSON S3 class: R6\n"}
In practice what I really want to do is load my ML model to predict a score with the plumber API. For example
model <- readRDS('model.rds') # Load model as a global variable
predict_score <- function(...) {
df_in <- match.call() %>%
as.list() %>%
.[2:length(.)] %>%
as.data.frame()
json_out <- list(
score_out = predict(model, df_in) %>%
toJSON(., pretty = T, auto_unbox = T)
return(json_out)
}
This function works as expected when running locally, but running through the API via curl -s --data 'var1=1&var2=2...etc' http://listen_address
I get the following error: {"error":"500 - Internal server error","message":"Error in as.data.frame.default(x[[i]], optional = TRUE): cannot coerce class \"c(\"PlumberResponse\", \"R6\")\" to a data.frame\n"}
Internally plumber match parameters in your request to the name of the parameters in your function. There are special arguments that you could use to explore all args in the request. If you have an argument named req, it will give you an environnement containing the entire request metadata, one of which is req$args. Which you could then parse. The first two args are self reference to special arguments req and res. They are environment and should not be serialized. I would not advise doing what is shown here in any production code as it opens up the api to abuse.
model <- readRDS('model.rds') # Load model as a global variable
#' #post /API
#' #serializer unboxedJSON
predict_score <- function(req) {
df_in <- as.data.frame(req$args[-(1:2)])
json_out <- list(
score_out = predict(model, df_in)
return(json_out)
}
But for your use case, what I would actually advise is having a single parameter named df_in. Here is how you would set that up.
model <- readRDS('model.rds') # Load model as a global variable
#' #post /API
#' #param df_in
#' #serializer unboxedJSON
predict_score <- function(df_in) {
json_out <- list(
score_out = predict(model, df_in)
return(json_out)
}
Then with curl
curl --header "Content-Type: application/json" \
--request POST \
--data '{"df_in":{"hi":2, "othercrap":4}}' \
http://listen_address
When the body of request starts with "{" plumber will parse the content of the body with jsonlite:fromJSON and use the name of the parsed objects to maps to parameters in your function.
Currently both CRAN and master branch on github do not handle this correctly via the swagger api but it will works just fine via curl or other direct calling method. Next plumber version will handle all that and more I believe.
See a similar answer to this of question here : https://github.com/rstudio/plumber/issues/512#issuecomment-605735332

R: How to save the content of a curl request as ascii text?

As a follow on from: How translate this curl command into a R curl call?
I can successful do a curl request but the returned result looks to be in a binary format, but the content should be ASCII. How do I write it out as an ASCII file?
require(httr)
headers = c(
`Content-Type` = 'text/csv'
)
data = upload_file('data/data.csv')
res <- httr::POST(url = 'https://some.url.com/invocations', httr::add_headers(.headers=headers), body = data)
content(res, "text")
works and if the output is JSON then
content(res, "parsed")
works too.

How to pass the curl -F parameter in the httr package?

Hi I try to translate this curl instruction using httr
curl -H "Authorization: Token f2210dacd9c6ccb8133606d94ff8e61d99b477fd" -F file=#test.txt -F filename=test.txt -F parent_dir=/ http://cloud.seafile.com:8082/upload-api/73c5d117-3bcf-48a0-aa2a-3f48d5274ae3
Without the -F parameter the instruction is :
httr::POST(
url = "http://cloud.seafile.com:8082/upload-api/73c5d117-3bcf-48a0-aa2a-3f48d5274ae3",
add_headers(Authorization = "Token f2210dacd9c6ccb8133606d94ff8e61d99b477fd")
)
)
I think I have to use the httr::upload_file function but I didn't manage to use this without error.
Do you have any idea how I can do that ?
Regards
Here is how to construct this curl request with httr package. I used httpbin.org to test the request sent.
You'll use POST filling the body with a list. encode argument controls how this list will be handle and by default it is the correct multipart you need.
res <- httr::POST(
url = "http://httpbin.org/post",
httr::add_headers(Authorization = "Token f2210dacd9c6ccb8133606d94ff8e61d99b477fd"),
# Add the file and metadata as body using a list. Default encode is ok
body = list(
file = httr::upload_file("test.txt"),
filename = "test.txt",
parent_dir = "/"
)
)
httr_ouput <- httr::content(res)
One way to check this is ok is to compare output with the curl command you know is working
out <- sys::exec_internal(
'curl -H "Authorization: Token f2210dacd9c6ccb8133606d94ff8e61d99b477fd" -F file=#test.txt -F filename=test.txt -F parent_dir=/ http://httpbin.org/post'
)
curl_output <- jsonlite::fromJSON(rawToChar(out$stdout))
#compare body
identical(curl_output$files, httr_ouput$files)
#> TRUE
identical(curl_output$form, httr_ouput$form)
#> TRUE
You can also do it with the crul package, another wrapper above curl; The logic is identical
con <- crul::HttpClient$new(
url = "http://httpbin.org/post"
)
crul_req <- con$post(
body = list(
file = crul::upload("test.txt"),
filename = "test.ext",
parent_dir = "/"
)
)
crul_output <- jsonlite::fromJSON(crul_req$parse())

Translating a curl command to R using httr (specifically '--data-binary #')

I am trying to transcribe some sound files to text using bing speech-to-text.
The following command works in command line (using git bash on Windows 10):
curl -v -X POST "https://speech.platform.bing.com/speech/recognition/interactive/
cognitiveservices/v1?language=<LANG>&format=detailed" -H "Transfer-Encoding:
chunked" -H "Ocp-Apim-Subscription-Key: <MY KEY>" -H "Content-type:
audio/wav; codec=audio/pcm; samplerate=16000" --data-binary #<MY .WAV-FILE>
I've tried this, but it doesnt work:
httr::POST(url = myURL,
add_headers("Ocp-Apim-Subscription-Key" = key,
"Content-type" = "audio/wav; codec=audio/pcm; samplerate=16000",
"Transfer-Encoding" = "chunked"),
body = (list("file" = upload_file("PATH_TO_FILE.wav"))),
verbose())
It returns this output:
Response
[https://speech.platform.bing.com/speech/recognition/dictation/
cognitiveservices/v1?language=<LANG>&format=detailed]
Date: 2017-11-29 13:29
Status: 200
Content-Type: text/plain
Size: 75 B
I believe that the request is related to the interpretation of the .wav file, and that I need to somehow add the '--data-binary' tag to the httr-request. I can see that my "content-type" is plain text, although i've specified. Furthermore: the API documentation specifies that i need to prefix my wav-file with an at-sign.
Any help would be much appreciated.
cheers.
EDIT: Link to API documentation
https://learn.microsoft.com/da-dk/azure/cognitive-services/speech/getstarted/getstartedrest?tabs=curl#tabpanel_AFC9x30-dR_curl
I figured it out.
The key is to set the proper MIME type in the body. Not setting this MIME type can result in wrongful interpretation on the receiving end, even though we get a response 200 back.
body <- list(file = httr::upload_file(
paste0(path, "/", f),
type = "audio/wav; codec=audio/pcm; samplerate=16000"))
where paste0(path, "/", f) is a path to an audio file.
myURL <- sprintf('https://speech.platform.bing.com/speech/recognition/%s/cognitiveservices/v1?language=%s&format=%s',
"dictation",
"da-DK",
"detailed")
rs <- httr::POST(
url = myURL,
httr::add_headers(.headers = c("Ocp-Apim-Subscription-Key" = key)),
httr::add_headers(.headers = c("Transfer-Encoding" = "chunked")),
body = body)

Resources