Let’s consider an example. We develop an application which calculates
factorial of a number:
library(RestRserve)
backend = BackendRserve$new()
application = Application$new()
application$add_get(path = "/factorial", function(.req, .res) {
  x = .req$get_param_query("x")
  x = as.integer(x)
  .res$set_body(factorial(x))
})Here is how request will be processed:
request = Request$new(
  path = "/factorial", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
response
#> <RestRserve Response>
#>   status code: 200 OK
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.4; Rserve/1.8.13Let’s take a closer look to the response object and its
body property:
As we can see it is a numeric value. HTTP response body however can’t
be an arbitrary R object. It should be something that external systems
can understand - either character vector or
raw vector. Fortunately application helps to
avoid writing boilerplate code to encode the body. Based on the
content_type property it can find encode
function which will be used to transform body into a http
body.
Two immediate questions can arise:
content_type is equal to text/plain?
content_type in
Application constructor. It is text/plain by
default, which means all the responses by default will have
text/plain content type.text/plain? Can
it encode any arbitrary content type?
ContentHandlers property. Out of the box it supports two
content types - text/plain and
application/json.For instance app1 and app2 are equal:
encode_decode_middleware = EncodeDecodeMiddleware$new()
app1  = Application$new(middleware = list())
app1$append_middleware(encode_decode_middleware)
app2 = Application$new()Here is example on how you can get the actual function used for
application/json encoding:
FUN = encode_decode_middleware$ContentHandlers$get_encode('application/json')
FUN
#> function(x, unbox = TRUE)  {
#>   res = jsonlite::toJSON(x, dataframe = 'columns', auto_unbox = unbox, null = 'null', na = 'null')
#>   unclass(res)
#> }
#> <bytecode: 0x7f93f3b58c40>
#> <environment: namespace:RestRserve>We can manually override application default content-type:
application$add_get(path = "/factorial-json", function(.req, .res) {
  x = as.integer(.req$get_param_query("x"))
  result = factorial(x)
  .res$set_body(list(result = result))
  .res$set_content_type("application/json")
})request = Request$new(
  path = "/factorial-json", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)And here is a little bit more complex example where we store a binary
object in the body. We will use R’s native serialization, but one can
use protobuf, messagepack, etc.
application$add_get(path = "/factorial-rds", function(.req, .res) {
  x = as.integer(.req$get_param_query("x"))
  result = factorial(x)
  body_rds = serialize(list(result = result), connection = NULL)
  .res$set_body(body_rds)
  .res$set_content_type("application/x-rds")
})However function above won’t work correctly. Out of the box
ContentHndlers doesn’t know anything about
application/x-rds:
request = Request$new(
  path = "/factorial-rds", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
response$body
#> [1] "500 Internal Server Error: can't encode body with content_type = 'application/x-rds'"In order to resolve problem above we would need to either register
application/x-rds content handler with
ContentHandlers$set_encode() or manually specify
encode function (identity in our case):
application$add_get(path = "/factorial-rds2", function(.req, .res) {
  x = as.integer(.req$get_param_query("x"))
  result = factorial(x)
  body_rds = serialize(list(result = result), connection = NULL)
  .res$set_body(body_rds)
  .res$set_content_type("application/x-rds")
  .res$encode = identity
})Now the answer is valid:
RestRserve facilitates with parsing incoming request body as well. Consider a service which expects JSON POST requests:
application = Application$new(content_type = "application/json")
application$add_post("/echo", function(.req, .res) {
  .res$set_body(.req$body)
})
request = Request$new(path = "/echo", method = "POST", body = '{"hello":"world"}', content_type = "application/json")
response = application$process_request(request)
response$body
#> [1] "{\"hello\":\"world\"}"The logic behind decoding is also controlled by
?EncodeDecodeMiddleware and its ContentHandlers
property.
Here is an example which demonstrates on how to extend ?EncodeDecodeMiddleware to handle additional content types:
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)
  }
)Extended middleware needs to be provided to the application constructor:
Now let’s test it:
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  setosaapp$add_post("/in", FUN = function(.req, .res) {
  str(.req$body)
})
req = Request$new(path = "/in", method = "POST", body = res$body, content_type = "text/csv")
app$process_request(req)
#> '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" ...
#> <RestRserve Response>
#>   status code: 200 OK
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.4; Rserve/1.8.13