rexyai / RestRserve

R web API framework for building high-performance microservices and app backends
https://restrserve.org
275 stars 32 forks source link

How to extend content_type to support "text/csv" #156

Closed DyfanJones closed 3 years ago

DyfanJones commented 4 years ago

Hi all,

Is there away to extend content_type when calling RestRserve endpoints?

Background in request

I am currently developing a package that interfaces into AWS sagemaker R6sagemaker. It is very much in early development however I have managed to get the basics working. I would like to use RestRserve to demonstrate R models on AWS SageMaker. By default AWS SageMaker uses the content_type "text/csv" when sending text files over to it's endpoints: AWS Sagemaker content types

Example:

I have developed an example: sagemaker restrserve example, however it fails due to the default "text/csv" content_type.

From reading your documentation I thought I could extend the content_type as follows:

# server.R
library(RestRserve)
library(data.table)

model_path = file.path(prefix, "model")
load(file.path(model_path, 'model.RData'))

encode_decode_middleware = EncodeDecodeMiddleware$new()
encode_decode_middleware$ContentHandlers$set_encode("text/csv",
     FUN = encode_decode_middleware$ContentHandlers$get_encode('text/plain'))

app = Application$new(middleware = list())
app$add_get("/ping",
            FUN = function(req, res) {
              res$set_body("R6sagemaker mars restrserve example")})

app$add_post("/invocations",
             FUN= function(req, res){
               res$set_content_type("text/csv")
               data = fread(req$body)

               result = predict(model, data, row.names=FALSE)
               res$set_body(result)
             }
)

app$append_middleware(encode_decode_middleware)

However I still get status code: 415 Unsupported Media Type. Is there away to extend content_type to include "text/csv"?

dselivanov commented 4 years ago

You are correct that you need to provide encode and decode functions for text/csv. You can take a look on how it is done for application/json here and here.

For text/csv content handler might look like (I use base connections and csv parsers here, but this can be amended using whatever you prefer):

library(RestRserve)

encode_decode_middleware = EncodeDecodeMiddleware$new()

encode_decode_middleware$ContentHandlers$set_encode(
  "text/csv", 
  function(x) {
    con = rawConnection(raw(0), "w")
    on.exit(close(con))
    write.csv(x, con, row.names = FALSE)
    rawConnectionValue(con)
  }
)

encode_decode_middleware$ContentHandlers$set_decode(
  "text/csv", 
  function(x) {
    res = try({
      con = textConnection(rawToChar(x), open = "r")
      on.exit(close(con))
      read.csv(con)
    }, silent = TRUE)

    if (inherits(res, "try-error")) {
      raise(HTTPError$bad_request(body = attributes(res)$condition$message))
    }
    return(res)
  }
)

And test

data(iris)
app = Application$new(middleware = list(encode_decode_middleware))

app$add_get("/iris", FUN = function(req, res) {
  res$set_content_type("text/csv")
  res$set_body(iris)
})

req = Request$new(path = "/iris", method = "GET")
res = app$process_request(req)

iris_out = read.csv(textConnection(rawToChar(res$body)))
head(iris_out)

Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3.0 1.4 0.2 setosa 3 4.7 3.2 1.3 0.2 setosa 4 4.6 3.1 1.5 0.2 setosa 5 5.0 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa

app$add_post("/in", FUN = function(req, res) {
  str(req$body)
})
req2 = Request$new(path = "/in", method = "POST", body = res$body, content_type = "text/csv")
res2 = app$process_request(req2)

'data.frame': 150 obs. of 5 variables: $ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ... $ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ... $ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ... $ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ... $ Species : chr "setosa" "setosa" "setosa" "setosa" ...

DyfanJones commented 4 years ago

@dselivanov thanks for your help. I have updated my example to include this. Feel free to close this ticket :)