Body encoding and decoding

Encoding

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/0.3.0

Let’s take a closer look to the response object and its body property:

str(response$body)
#>  chr "3628800"

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.

response$content_type
#> [1] "text/plain"
response$encode
#> NULL

Two immediate questions can arise:

  1. Why content_type is equal to text/plain?
    • This is because we can specify default content_type in Application constructor. It is text/plain by default, which means all the responses by default will have text/plain content type.
  2. How does application know how to encode text/plain? Can it encode any arbitrary content type?
    • Application by default is initialized with pre-defined [EncodeDecodeMiddleware] middleware. The logic on how to encode and decode request and response body is controlled by its 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: 0x468bc00>
#> <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)
response$body
#> [1] "{\"result\":3628800}"

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:

request = Request$new(
  path = "/factorial-rds2", 
  method = "GET", 
  parameters_query = list(x = 10)
)
response = application$process_request(request)
unserialize(response$body)
#> $result
#> [1] 3628800

Decoding

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.