forked from rstudio/apis-plumber
-
Notifications
You must be signed in to change notification settings - Fork 0
/
04-rendering-output.Rmd
203 lines (130 loc) · 13.8 KB
/
04-rendering-output.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# Rendering Output {#rendering-and-output}
```{r, echo=FALSE, out.width="45%", out.extra = 'style="float:right;"'}
knitr::include_graphics("files/images/plumber_output.png")
```
## The Response Object {#response-object}
// TODO
## 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()`
`@serializer htmlwidget` | `text/html; charset=utf-8` | `htmlwidgets::saveWidget()`
`@serializer unboxedJSON` | `application/json` | `jsonlite::toJSON(unboxed=TRUE)`
### 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](#response-object) from an endpoint. For example, consider the following API.
```{r, echo=FALSE, results='asis'}
include_r("files/apis/04-01-response.R")
```
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.
```{r, echo=FALSE, results='asis'}
include_r("files/apis/04-02-contenttype.R")
```
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).
### Boxed vs Unboxed JSON
You may have noticed that API responses generated from Plumber render singular values (or "scalars") as arrays. For instance:
```{r}
jsonlite::toJSON(list(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.
```{r, echo=FALSE, results='asis'}
include_r("files/apis/04-03-letters.R")
```
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:
```{r, echo=FALSE, results='asis'}
pr <- plumber::plumb("files/apis/04-03-letters.R")
e <- pr$endpoints[[1]][[1]]
code_chunk(json_serialize(e$exec(letter="U")), "json")
```
However, http://localhost:8000/boxed?letter=Y will produce:
```{r, echo=FALSE, results='asis'}
pr <- plumber::plumb("files/apis/04-03-letters.R")
e <- pr$endpoints[[1]][[1]]
code_chunk(jsonlite::toJSON(e$exec(letter="Y"), auto_unbox = FALSE), "json")
```
while http://localhost:8000/unboxed?letter=Y will produce:
```{r, echo=FALSE, results='asis'}
e <- pr$endpoints[[1]][[2]]
code_chunk(jsonlite::toJSON(e$exec(letter="Y"), auto_unbox = TRUE))
```
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.
### Customizing Image Serializers
The `@jpeg` and `@png` annotations cause the graphical output of an endpoint to be written to a file then returned to the client using the `jpeg()` or `png()` functions, respectively. These functions both accept a variety of additional options that customize the output including `width`, `height`, and `bg` among others.
As of version 0.4.3 of plumber, these annotations now accept additional arguments that will be passed into these functions. This enables the creation of endpoints like:
```{r, echo=FALSE, results='asis'}
include_r("files/apis/04-04-image.R")
```
At a lower level, the arguments inside the parentheses will be used as the arguments to a `list()` call. Meaning that any R code that can be prefixed by `list` to form a valid R expression can be used. For example, `#' @png (width=2^10 + 1)` would be a valid annotation. This code is evaluated once when your API is `plumb()`d.
While this approach can be used to statically define the size of an image, it will not work for dynamic sizing of an image. If you wish to dynamically size images, you will need render and capture the graphical output yourself and return the contents with the appropriate `Content-Type` header. See the existing image renderers as a model of how to do this.
## Error Handling
Plumber wraps each endpoint invocation so that it can gracefully capture errors.
```{r, echo=FALSE, results='asis'}
include_r("files/apis/04-05-error.R")
```
If you run this API and visit http://localhost:8000/simple, you'll notice two things:
1. 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"]}`
2. 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.
```{r, echo=FALSE, results='asis'}
pr <- plumber::plumb("files/apis/04-05-error.R")
e <- pr$endpoints[[1]][[2]]
code_chunk(json_serialize(e$exec(res=list(status=1))), "json")
```
// TODO: example of setErrorHandler on router.
## Custom Serializers
// TODO
## Setting Cookies {#setting-cookies}
As part of fulfilling a request, a Plumber API can choose to set HTTP cookies on the client. HTTP APIs don't implicitly contain a notion of a "session." Without some additional information, Plumber has no way of ascertaining whether or not two HTTP requests that come in are associated with the same user. Cookies offer a way to commission the client to store some state on your behalf so that selected data can outlive a single HTTP request; the full implications of using cookies to track state in your API are discussed [here](#state-cookies). The two forms of Plumber cookies -- plain-text and encrypted -- are discussed in the following sections.
Before you make cookies an important part of your API's security model, be sure to understand the section on the [security considerations when working with cookies](#security-cookies).
### Setting Unencrypted Cookies
Plumber can both set and receive plaint-text cookies. The API endpoint below will return a random letter, but it remembers your preferences on whether you like capitalized or lower-case letters.
```{r, echo=FALSE, results='asis'}
include_r("files/apis/06-01-capitalize.R")
```
Since this API is using a `PUT` request to test this API, we'll use `curl` on the command line to test it. (There's nothing about cookies that necessitates `PUT` requests; you could just as easily modify this API to use a `GET` request.) We can start by visiting the `/letter` endpoint and we'll see that the API defaults to a lower-case alphabet. `curl http://localhost:8000/letter`
```{r, echo=FALSE, results='asis'}
pr <- plumber::plumb("files/apis/06-01-capitalize.R")
e <- pr$endpoints[[1]][[2]]
code_chunk(json_serialize(e$exec(req=list(cookies=list()))), "json")
```
If we send a `PUT` request and specify the `capital` parameter, a cookie will be set on the client which will allow the server to accommodate our preference in future requests. In `curl`, you need to specify a file in which you want to save these cookies using the `-c` option. This is a good reminder that clients handle cookies differently -- some won't support them at all -- so be sure that the clients you intend to support with your API play nicely with cookies if you want to use them.
To send a `PUT` request, setting the parameter `capital` to `1`, we could invoke: `curl -c cookies.txt -X PUT --data 'capital=1' "http://localhost:8000/preferences"`. If you print out the `cookies.txt` file, you should now see that it contains a single cookie called `capitalize` with a value of `1`.
We can make another `GET` request to `/letter` to see if it accommodates our preferences. But we'll need to tell `curl` to use the cookies file we just created when sending this request using the `-b` switch: `curl -b cookies.txt http://localhost:8000/letter`. You should now see that the API is returning a random capitalized letter.
The `setCookie` method accepts a variety of additional options to customize how the cookie should be handled by the client. By default, cookies are set with a `session` lifetime, meaning that the cookie will persist in the user's browser until the client closes the tab at which point the cookie will be deleted. You can customize this by setting the `expiration` parameter in `setCookie` using either a number of seconds in the future in which this cookie should expire. Alternatively, you can provide an object of class `POSIXt`, in which case that will be interpreted as the time at which the cookie should expire.
Other options that can be set on the cookie include `path` (the path on your domain at which the cookie should be installed on the client); `http` (controls whether or not the cookie should be accessible to JavaScript running on this domain -- where `TRUE` means that the cookie is HTTP-only, and not accessible from JavaScript); and `secure` (if `TRUE`, instructs the browser to only send the cookie over HTTPS, not insecure HTTP.
If you're using cookies to infer any security-sensitive properties (such as to identify a user, or determine what resources this client should have access to), be sure to see the [Security chapter](#security) -- in particular [the section on the security implications of cookies](#security-cookies).
### Setting Encrypted Cookies {#encrypted-cookies}
In addition to storing plain-text cookies, Plumber also supports handling cookies that are encrypted. Encrypted cookies prevent your users from seeing what is stored inside of them and also sign their contents so that users can't modify what is stored.
To use this feature, you must explicitly add it to your router after constructing it. For example, you could run the following sequence of commands to create a router that supports encrypted session cookies.
```r
pr <- plumb("myfile.R")
pr$registerHooks(sessionCookie("mySecretHere", "cookieName"))
pr$run()
```
You'll notice the above example is using the `sessionCookie` hooks that come with Plumber. By adding registering these hooks on your router, you'll ensure that the `req$session` object is made available on incoming requests and is persisted to the cookie named `cookieName` when the response is ready to be sent to the user. In this example, the key used to encrypt the data is `"mySecretHere"`, which is obviously a very weak secret key.
Unlike `res$setHeader()`, the values attached to `req$session` *are* serialized via `jsonlite`; so you're free to use more complex data structures like lists in your session. Also unlike `res$setHeaders()`, `req$session` encrypts the data using the secret key you provide as the first argument to the `sessionCookie()` function.
As an example, we'll store an encrypted cookie that counts how many times this client has visited a particular endpoint:
```r
#* @get /sessionCounter
function(req){
count <- 0
if (!is.null(req$session$counter)){
count <- as.numeric(req$session$counter)
}
req$session$counter <- count + 1
return(paste0("This is visit #", count))
}
```
Again, you would need to register the `sessionCookie()` hooks on your router before this code would work.
If you inspect the cookie being set in your browser, you'll find that its value is encrypted by the time it gets to the client. But by the time it arrives in Plumber, your cookie is available as a regular R list and can be read or modified.