--- title: "Body encoding and decoding" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Body encoding and decoding} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} 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: ```{r} 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: ```{r} 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: ```{r} 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. ```{r} response$content_type ``` ```{r} 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. 1. 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: ```{r} 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: ```{r} FUN = encode_decode_middleware$ContentHandlers$get_encode('application/json') FUN ``` We can manually override application default content-type: ```{r} 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") }) ``` ```{r} request = Request$new( path = "/factorial-json", method = "GET", parameters_query = list(x = 10) ) response = application$process_request(request) ``` ```{r} 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. ```{r} 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`: ```{r} 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): ```{r} 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: ```{r} 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: ```{r} 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: ```{r} 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: ```{r} data(iris) app = Application$new(middleware = list(encode_decode_middleware)) ``` Now let's test it: ```{r} 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) ``` ```{r} 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) ```