Chapter 4 Rendering Output
4.1 The Response Object
// TODO
4.2 Serializers
In order to send a response from R to an API client, the object must be “serialized” into some format that the client can understand. JavaScript Object Notation (JSON) is one standard which is commonly used by web APIs. JSON serialization translates R objects like list(a=123, b="hi!") to JSON text resembling {a: 123, b: "hi!"}.
JSON is not appropriate for every situation, however. If you want your API to render an HTML page that might be viewed in a browser, for instance, you will need a different serializer. Likewise, if you want to return an image rendered in R, you likely want to use a standard image format like PNG or JPEG rather than JSON.
By default, Plumber serializes objects into JSON via the jsonlite R package. However, there are a variety of other serializers that are built in to the package.
| Annotation | Content Type | Description/References |
|---|---|---|
@json |
application/json |
jsonlite::toJSON() |
@html |
text/html; charset=utf-8 |
Passes response through without any additional serialization |
@jpeg |
image/jpeg |
jpeg() |
@png |
image/png |
png() |
@htmlwidget |
text/html; charset=utf-8 |
htmlwidgets::saveWidget() |
@unboxedJSON |
application/json |
jsonlite::toJSON(unboxed=TRUE) |
4.2.1 Bypassing Serialization
In some instances it may be desirable to return a value directly from R without serialization. You can bypass serialization by returning the response object from an endpoint. For example, consider the following API.
#' Endpoint that bypasses serialization
#' @get /
function(res){
res$body <- "Literal text here!"
res
}The response that is returned from this endpoint would contain the body Literal text here! with no Content-Type header and without any additional serialization.
Similarly, you can leverage the @serializer contentType annotation which does no serialization of the response but specifies the contentType header. You can use this annotation when you want more control over the response that you send.
#* @serializer contentType list(type="application/pdf")
#* @get /pdf
function(){
tmp <- tempfile()
pdf(tmp)
plot(1:10, type="b")
text(4, 8, "PDF from plumber!")
text(6, 2, paste("The time is", Sys.time()))
dev.off()
readBin(tmp, "raw", n=file.info(tmp)$size)
}Running this API and visiting http://localhost:8000/pdf will download the PDF generated from R (or display the PDF natively, if your client supports it).
4.2.2 Boxed vs Unboxed JSON
You may have noticed that API responses generated from Plumber render singular values (or “scalars”) as arrays. For instance:
jsonlite::toJSON(list(a=5))## {"a":[5]}
The value of the a element, though it’s singular, is still rendered as an array. This may surprise you initially, but this is done to keep the output consistent. While JSON differentiates scalar from vector objects, R does not. This creates ambiguity when serializing an R object to JSON since it is unclear whether a particular element should be rendered as an atomic value or a JSON array.
Consider the following API which returns all the letters lexicographically “higher” than the given letter.
#' Get letters after a given letter
#' @get /boxed
function(letter="A"){
LETTERS[LETTERS > letter]
}
#' Get letters after a given letter
#' @serializer unboxedJSON
#' @get /unboxed
function(letter="A"){
LETTERS[LETTERS > letter]
}This is an example of an API that, in some instance, produces a scalar, and in other instances produces a vector.
Visiting http://localhost:8000/boxed?letter=U or http://localhost:8000/unboxed?letter=U will return identical responses:
["V", "W", "X", "Y", "Z"]However, http://localhost:8000/boxed?letter=Y will produce:
["Z"]while http://localhost:8000/unboxed?letter=Y will produce:
"Z"
The /boxed endpoint, as the name implies, produces “boxed” JSON output in which length-1 vectors are still rendered as an array. Conversely, the /unboxed endpoint sets auto_unbox=TRUE in its call to jsonlite::toJSON, causing length-1 R vectors to be rendered as JSON scalars.
While R doesn’t distinguish between scalars and vectors, API clients may respond very differently when encountering a JSON array versus an atomic value. You may find that your API clients will not respond gracefully when an object that they expected to be a vector becomes a scalar in one call.
For this reason, Plumber inherits the jsonlite::toJSON default of setting auto_unbox=FALSE which will result in all length-1 vectors still being rendered as JSON arrays. You can configure an endpoint to use the unboxedJSON serializer (as shown above) if you want to alter this behavior for a particular endpoint.
There are a couple of functions to be aware of around this feature set. If using boxed JSON serialization, jsonlite::unbox() can be used to force a length-1 object in R to be presented in JSON as a scalar. If using unboxed JSON serialization, I() will cause a length-1 R object to present as a JSON array.
4.3 Error Handling
Plumber wraps each endpoint invocation so that it can gracefully capture errors.
#' Example of throwing an error
#' @get /simple
function(){
stop("I'm an error!")
}
#' Generate a friendly error
#' @get /friendly
function(res){
msg <- "Your request did not include a required parameter."
res$status <- 400 # Bad request
list(error=jsonlite::unbox(msg))
}If you run this API and visit http://localhost:8000/simple, you’ll notice two things:
- An HTTP response with a status code of
500(“internal server error”) is sent to the client. You should see an error message resembling:{"error":["500 - Internal server error"],"message":["Error in (function () : I'm an error!\n"]} - A similar error is printed in the terminal where you’re running your Plumber API.
This means that it is possible for you to intentionally stop() in an endpoint or a filter as a way to communicate a problem to your user. However, it may be preferable to render errors from your API in a consistent format with more helpful error messages.
{
"error": "Your request did not include a required parameter."
}4.4 Custom Serializers
// TODO