Body encoding and decoding

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>", 
  results = 'markup'
)

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

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

str(response$body)

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
response$encode

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

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

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

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)

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

The logic behind decoding is also controlled by ?EncodeDecodeMiddleware and its ContentHandlers property.

Extending encoding and decoding

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:

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

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)
app$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)


Try the RestRserve package in your browser

Any scripts or data that you put into this service are public.

RestRserve documentation built on Sept. 12, 2022, 9:06 a.m.