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