I am trying to pass header "X-Name" from postman then access it from API which is build using Plumber R.
However, I am not able to access it. using following code,
req$HTTP_X-NAME, then i am getting following error.
Error in (function (id, time_frame, req, res) : object 'name' not
found\n"
If I use following code, I get NULL values,
req$HTTP_X_NAME
or
req[["HTTP_X-NAME"]
or
req[["HTTP_X-Name"]
Following is my code:
#* #get /
healthCheck <- function(){
print("It's Running")
}
#* #param id:int Random Number
#* #post /test/values
#* #get /test/values
test <- function(id, req, res){
print(req[["HTTP_X-Name"]])
return(req[["HTTP_X-Name"]])
}
Related
I have followed the Rstudio example here for logging for logging requests in Plumber (R API package) and would like to add other variables to the log. However, the registerHooks statement does not recognise global variables (<<-).
# Enable CORS Filtering
#' #filter auth_filter
auth_filter <- function(req, res) {
req_user <<- req$HEADERS['authorization'] %>% as.character()
req_tenant <<- req$HTTP_TENANT
}
pr$registerHooks(
list(
preroute = function() {
# Start timer for log info
tictoc::tic()
},
postroute = function(req, res) {
end <- tictoc::toc(quiet = TRUE)
# Log details about the request and the response
log_info('{convert_empty(req_user)} {convert_empty(req_tenant)} {convert_empty(req$REMOTE_ADDR)} "{convert_empty(req$HTTP_USER_AGENT)}" {convert_empty(req$HTTP_HOST)} {convert_empty(req$REQUEST_METHOD)} {convert_empty(req$PATH_INFO)} {convert_empty(res$status)} {round(end$toc - end$tic, digits = getOption("digits", 5))}')
}))
In the above example, req_user and req_tenant change for every request. The above example gives an error message, stating that req_user and req_tenant do not exist. I have also tried preserialize as an alternative to postroute. How can these variables be logged? The don't need to be global, this was just an additional attempt to solve the problem.
I believe the error is produced because of the convert_empty function not being able to handle null's or na's.
I adjusted this function to handle null's or na's in case they do occur:
convert_empty <- function(string) {
if (is.null(string) || is.na(string) || string == "") {
"-"
} else {
string
}
}
Using the sample plumber.R code that's produced in RStudio, this should work:
#Plumber.R file
library(plumber)
library(logger)
# Specify how logs are written
log_dir <- "logs"
if (!fs::dir_exists(log_dir)) fs::dir_create(log_dir)
log_appender(appender_tee(tempfile("plumber_", log_dir, ".log")))
#* Return the sum of two numbers
#* #param a The first number to add
#* #param b The second number to add
#* #post /sum
function(a, b) {
as.numeric(a) + as.numeric(b)
}
Then we register the hooks as follows:
library(plumber)
pr <- plumb("plumber.R")
convert_empty <- function(string) {
if (is.null(string) || is.na(string) || string == "") {
"-"
} else {
string
}
}
pr$registerHooks(
list(
preroute = function() {
# Start timer for log info
tictoc::tic()
},
postroute = function(req, res) {
end <- tictoc::toc(quiet = TRUE)
# Log details about the request and the response
log_info('{convert_empty(as.character(req$HEADERS["authorization"]))} {convert_empty(req$HTTP_TENANT)} {convert_empty(req$REMOTE_ADDR)} {convert_empty(req$HTTP_USER_AGENT)} {convert_empty(req$HTTP_HOST)} {convert_empty(req$REQUEST_METHOD)} {convert_empty(req$PATH_INFO)} {convert_empty(res$status)} {round(end$toc - end$tic, digits = getOption("digits", 5))}')
}
)
)
pr
In the console, you can do:
pr$run()
so serve your API locally.
From there, go to the terminal in RStudio and do a curl. Assuming is your port, an example would be:
curl -H "Authorization: Bearer my_token" -H "TENANT: 123" -X POST "http://127.0.0.1:9520/sum?a=1&b=2"
You should see a return of 3 in the terminal, and if you look at the R console, you will see the Authorization and tenant headers logged
So it turns out my specific problem was actually the format of tenant name, e.g. req$HTTP_TENANT = "Company A/S". When this is passed by log_info, it gets split into two columns, which is more than the logging table expects. Maybe the forward slash is escaping something.
For some reason, you have to put it in quotes in log_info, so log_info('"{convert_empty(req$HTTP_TENANT)}"') instead of log_info('{convert_empty(req$HTTP_TENANT)}') as with req$HTTP_USER_AGENT in the original post.
My plumber function runs properly in local ( creating two R sessions and sending post request from one to other works ). However when doing the same for digitalocean server it is returning empty sting.
Plumber API
# plumber.R
#* #post /
#* #get /
function(req){
tryCatch({
list( rawbody = req$postBody )
}, error = function(e) {
return( jsonlite::toJSON( list('error' = e), force = T ) )
})
}
POST request
rm( list = ls() )
library(httr)
library(jsonlite)
options(stringsAsFactors = FALSE)
# url for local testing
url <- "http://127.0.0.1:6531" #... have changed URL to server url when testing for server
body = list( 'msg' = 'hi' )
# set API path
path <- 'data'
# send POST Request to API
raw.result <- POST(url = url, path = path, body = body, encode = 'json')
# check status code
raw.result$status_code
# check response
fromJSON(rawToChar(raw.result$content))
Please note GET request works fine but sending data with JSON doesn't work.
Output for server url
fromJSON(rawToChar(raw.result$content))
$rawbody
[1] ""
Output from local url
fromJSON(rawToChar(raw.result$content))
$rawbody
[1] "hi"
What am I doing wrong ?
I have created a dummy model using the below code :
#get the data
data(Boston, package="MASS")
# train a model for median house price as a function of the other variables
bos_rf <- lm(medv ~ crim + indus + dis , data=Boston)
# save the model
saveRDS(bos_rf, "bos_rf.rds")
Now i want to expose this model as an API using plumber. For this my code is
# load as bos_rf.R
bos_rf <- readRDS("bos_rf.rds")
#* #param input_json JSON file
#* #post /score
function(input_json)
{
temp <- toJSON(input_json, auto_unbox = T)
data <- fromJSON(temp) %>% as.data.frame
data = data %>% mutate_all(as.numeric)
predict(bos_rf, data)
}
Above my param is a JSON , i am keen to actually keep it as a data frame . I am converting the JSON to data frame in the function
Then i start the API using
# try API 1
#
dummy_model_api <- plumber::plumb("2_R_code_to_API.R")
dummy_model_api$run(host = '127.0.0.1', port = 8000)
API runs fine when i paste the JSON in the swagger portal , but when i run curl using below commands
$ curl "http://127.0.0.1:8000/score" -d "#test.JSON"
$ curl --data #test.json http://localhost:8000/score
None work. how do i directly pass the test JSON to the API to get a prediction. Note that if i check the function with R i get the prediction. Kindly advise how can one pass a JSON or DF directly to curl API request and get a response vs manually copying json / or defining API inputs with each variable one by one. Such a method is not feasible with 100's of variables.
How can a sample of this JSON also reflect in the swagger body already. i.e. above when the swagger opens , a sample JSON is already there in body with some values and ready to execute.
plumber will execute fromJSON on the request body if it dectects it starts with {.
What you would normally do is send a JSON string like
{
"input_json" : _insert toJSON results here_
}
So that plumber can turn that into a named list and map input_json to your function parameter.
How do I define asynchronous API endpoints in plumber?
I didn't really find plumber-specific documentation on the topic except this example and this GitHub issue
When I try to reproduce the example, I get an error that R doesn't know how to turn a promise into JSON (at least that's what I think the problem is):
<simpleError: No method asJSON S3 class: promise>
Example
library(promises)
sleep_count <- 5
# add 5 seconds of sleep time
add_async_sleep <- function(p) {
n <- 20
for (i in 1:(sleep_count * n)) {
p <- then(p, function(value) {
Sys.sleep(1/n)
"" # return value
})
}
p
}
# use name_ as a placeholder for name when there are extra args
time <- function(name_, name = name_) {
paste0(name, ": ", Sys.time())
}
new_promise <- function() {
promise(function(resolve, reject){ resolve(NULL) })
}
#' #get /async
function() {
new_promise() %>%
add_async_sleep() %...>%
time("async")
}
Say this code lives in file plumber.R, then you should be able to start the API server and bring up Swagger with
r <- plumber::plumb(here::here("plumber.R"))
r$run()
Once I try out the endpoint /async, my R console reports
Starting server to listen on port 7361
Running the swagger UI at http://127.0.0.1:7361/__swagger__/
<simpleError: No method asJSON S3 class: promise>
and Swagger looks like this:
Disclaimer
I'm new to future and promises and only made it mid-way through the docs on https://rstudio.github.io/promises/ yet.
I am trying to use ellipsis with plumber as input json may vary
#' #post /predict
calculate_prediction <- function(...){
arguments =list(...)
print(arguments)
return(arguments)
This throws me error below :
<simpleError: No method asJSON S3 class: R6>
How to resolve this issue
The issue with using ... in your function is that the first two arguments are the request body and the response body that are passed in the API call. If you add those in first, you can capture all the remaining unnamed args.
#' #post /predict
calculate_prediction <- function(req, res, ...){
arguments = list(...)
print(arguments)
return(arguments)
}