Let’s consider an example. We develop an application which calculates factorial
of a number:
library(RestRserve)
= BackendRserve$new()
backend = Application$new()
application $add_get(path = "/factorial", function(.req, .res) {
application= .req$get_param_query("x")
x = as.integer(x)
x $set_body(factorial(x))
.res })
Here is how request will be processed:
= Request$new(
request path = "/factorial",
method = "GET",
parameters_query = list(x = 10)
)= application$process_request(request)
response
response#> <RestRserve Response>
#> status code: 200 OK
#> content-type: text/plain
#> <Headers>
#> Server: RestRserve/1.2.0; Rserve/1.8.6
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.
$content_type
response#> [1] "text/plain"
$encode
response#> NULL
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:
= EncodeDecodeMiddleware$new()
encode_decode_middleware = Application$new(middleware = list())
app1 $append_middleware(encode_decode_middleware)
app1
= Application$new() app2
Here is example on how you can get the actual function used for application/json
encoding:
= encode_decode_middleware$ContentHandlers$get_encode('application/json')
FUN
FUN#> function(x, unbox = TRUE) {
#> res = jsonlite::toJSON(x, dataframe = 'columns', auto_unbox = unbox, null = 'null', na = 'null')
#> unclass(res)
#> }
#> <bytecode: 0x7fdf450f2398>
#> <environment: namespace:RestRserve>
We can manually override application default content-type:
$add_get(path = "/factorial-json", function(.req, .res) {
application= as.integer(.req$get_param_query("x"))
x = factorial(x)
result $set_body(list(result = result))
.res$set_content_type("application/json")
.res })
= Request$new(
request path = "/factorial-json",
method = "GET",
parameters_query = list(x = 10)
)= application$process_request(request) response
$body
response#> [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.
$add_get(path = "/factorial-rds", function(.req, .res) {
application= as.integer(.req$get_param_query("x"))
x = factorial(x)
result = serialize(list(result = result), connection = NULL)
body_rds $set_body(body_rds)
.res$set_content_type("application/x-rds")
.res })
However function above won’t work correctly. Out of the box ContentHndlers
doesn’t know anything about application/x-rds
:
= Request$new(
request path = "/factorial-rds",
method = "GET",
parameters_query = list(x = 10)
)= application$process_request(request)
response $body
response#> [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):
$add_get(path = "/factorial-rds2", function(.req, .res) {
application= as.integer(.req$get_param_query("x"))
x = factorial(x)
result = serialize(list(result = result), connection = NULL)
body_rds $set_body(body_rds)
.res$set_content_type("application/x-rds")
.res$encode = identity
.res })
Now the answer is valid:
= Request$new(
request path = "/factorial-rds2",
method = "GET",
parameters_query = list(x = 10)
)= application$process_request(request)
response unserialize(response$body)
#> $result
#> [1] 3628800
RestRserve facilitates with parsing incoming request body as well. Consider a service which expects JSON POST requests:
= Application$new(content_type = "application/json")
application $add_post("/echo", function(.req, .res) {
application$set_body(.req$body)
.res
})
= Request$new(path = "/echo", method = "POST", body = '{"hello":"world"}', content_type = "application/json")
request = application$process_request(request)
response $body
response#> [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:
= EncodeDecodeMiddleware$new()
encode_decode_middleware
$ContentHandlers$set_encode(
encode_decode_middleware"text/csv",
function(x) {
= rawConnection(raw(0), "w")
con on.exit(close(con))
write.csv(x, con, row.names = FALSE)
rawConnectionValue(con)
}
)
$ContentHandlers$set_decode(
encode_decode_middleware"text/csv",
function(x) {
= try({
res = textConnection(rawToChar(x), open = "r")
con 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)
= Application$new(middleware = list(encode_decode_middleware)) app
Now let’s test it:
$add_get("/iris", FUN = function(.req, .res) {
app$set_content_type("text/csv")
.res$set_body(iris)
.res
})
= Request$new(path = "/iris", method = "GET")
req = app$process_request(req)
res
= read.csv(textConnection(rawToChar(res$body)))
iris_out 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
$add_post("/in", FUN = function(.req, .res) {
appstr(.req$body)
})= Request$new(path = "/in", method = "POST", body = res$body, content_type = "text/csv")
req $process_request(req)
app#> '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.0; Rserve/1.8.6