0

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"}

Matt Elgazar
  • 707
  • 1
  • 8
  • 21

1 Answers1

1

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

Bruno Tremblay
  • 756
  • 4
  • 9
  • Is there way I can do this without having to change the curl request? Ideally I would change the code to handle curl -s --data 'myvar1=2&myvar2=3' listen_address – Matt Elgazar May 16 '20 at 04:02
  • yes, using a 'req' args and retrieving all args from req$args. – Bruno Tremblay May 16 '20 at 14:53
  • Also, yon don't need to do toJSON since the serializer will do that for you. – Bruno Tremblay May 16 '20 at 15:02
  • Thanks for your help. The second one works but the first function does not. Here is my function ```#' @post /API #' @serializer unboxedJSON predict_score <- function(req) { return(req$args)}``` While it's listening I typed in the terminal curl -s --data 'hi=2' listen_address and got ```an exception occurred``` I also tried just returning req. – Matt Elgazar May 17 '20 at 01:49
  • put a `browser()` call before `return(req$args)` line. Run it and call it with curl. It should enter debug mode and let you explore what is available to you at execution time. Like I said, `req$args` contains environments so you won't be able to serialize them. I still would not recommend doing that as you expose your API to injection and unprotected behavior. – Bruno Tremblay May 17 '20 at 16:33
  • Great thanks for your help! Do you know why that command results in potential Api injection? – Matt Elgazar May 18 '20 at 01:07
  • It's just bad practice to have an API without an API definition, it is hard to document and to share with others. But at the same time, you make decision based the environment you evolve in. You are a better judge of the risk you are exposed to. – Bruno Tremblay May 18 '20 at 13:37