#* Enable Cross-origin Resource Sharing
#* @filter cors
# This is more complex than what's in the official documentation
# (https://www.rplumber.io/articles/security.html#cross-origin-resource-sharing-cors)
# because it correctly allows requests to come from http://localhost too
# (via https://github.com/rstudio/plumber/issues/66#issuecomment-418660334)
<- function(req, res) {
cors $setHeader("Access-Control-Allow-Origin", "*")
res
if (req$REQUEST_METHOD == "OPTIONS") {
$setHeader("Access-Control-Allow-Methods", "*")
res$setHeader("Access-Control-Allow-Headers", req$HTTP_ACCESS_CONTROL_REQUEST_HEADERS)
res$status <- 200
resreturn(list())
else {
} ::forward()
plumber
} }
7 Annotations, error handling, and CORS
Before creating actual endpoints, we should add a little bit of additional infrastructure to the API first to make it easier to work with and allow it to be accessed from other computers.
7.2 Error handling
In our histogram plotting function, we’ve added a little safeguard to make sure people don’t pass numbers that are too big:
if (n >= 10000) {
stop("`n` is too big. Use a number less than 10,000.")
}
If you pass a huge number, you’ll get a JSON response like this:
{
"error": "500 - Internal server error",
"message": "Error in (function (n = 100) : `n` is too big. Use a number less than 10,000.\n"
}
That’s nice, but the response code (500) is a little too generic. The HTTP protocol has a ton of more specific response codes. For instance, 200 means everything worked, while 404 means the response couldn’t be found (you’ve seen 404 errors in the wild all the time). Right now we’re returning a 500 response, which is a generic catch-all response to any kind of issue. In this case, passing a number that is too big actually fits one of the standard HTTP responses: it’s a bad request, which is code 400. It’d be nice if we could use that instead of the generic 500.
Also, the automatically generated message here is too messy. A user doesn’t know that something like function (n = 100)
is happening behind the scenes. This message is actually revealing some of the R code to the user, which probably isn’t great. It would be nice to have a cleaner (and safer) message!
Fortunately Aaron Jacobs, currently a software engineer at Posit, has made a nice way to more gracefully handle these HTTP errors. See his post for all the details. For the sake of brevity, I’ll just show the final code here. We need to add all this:
# Custom error handling
# https://web.archive.org/web/20240110015732/https://unconj.ca/blog/structured-errors-in-plumber-apis.html
# Helper function to replace stop()
<- function(message, status) {
api_error <- structure(
err list(message = message, status = status),
class = c("api_error", "error", "condition")
)signalCondition(err)
}
# General error handling function
<- function(req, res, err) {
error_handler if (!inherits(err, "api_error")) {
$status <- 500
res$body <- jsonlite::toJSON(auto_unbox = TRUE, list(
resstatus = 500,
message = "Internal server error."
))$setHeader("content-type", "application/json") # Make this JSON
res
# Print the internal error so we can see it from the server side. A more
# robust implementation would use proper logging.
print(err)
else {
} # We know that the message is intended to be user-facing.
$status <- err$status
res$body <- jsonlite::toJSON(auto_unbox = TRUE, list(
resstatus = err$status,
message = err$message
))$setHeader("content-type", "application/json") # Make this JSON
res
}
res
}
#* @plumber
function(pr) {
# Use custom error handler
%>% pr_set_error(error_handler)
pr }
Now, go and change stop()
in the histogram endpoint to api_error()
:
if (n >= 10000) {
api_error("`n` is too big. Use a number less than 10,000.", 400)
}
Rerun the API and use the documentation page to pass a huge number to /plot
. You’ll get a much nicer error now with 400 status code:
{
"status": 400,
"message": "`n` is too big. Use a number less than 10,000."
}
7.3 CORS
Right now, for security reasons, the server that {plumber} creates will only allow people to access it from the same domain. Like, if I hosted the {plumber} server at api.example.com
, I could create a website or dashboard at www.example.com
and access the API from it just fine. But if I created a website at www.andrewheiss.com
and made an Observable JS script that sent a request to api.example.com
, it wouldn’t work. That’s a cross-domain request, and cross-origin resourse sharing (CORS) is disabled by default.
If you like that, cool—leave everything the way it is. Disallowing cross-origin resource sharing (CORS) is super common for APIs that you want to be more restricted.
But in this case, I want to be able to use the API from anywhere, including from local servers on my computer. When you render a Quarto document, for instance, it is served at http://localhost:SOME_PORT
. If you’re accessing a {plumber} API online and it has CORS disabled, you won’t be able to use the API from your local document, since localhost
is not the same domain as example.com
.
The {plumber} documentation provides a short code snippet that adds a filter to allow CORS. However, it doesn’t work with localhost
domains, since those are special. After lots of googling and experimenting, I found a more complete {plumber} filter for enabling CORS for all domains, including localhost
URLs. Here it is:
Include that in plumber.R
and you’ll have better CORS support.
7.4 Current plumber.R
file
Here’s what the API is looking like now with our extra annotations, error handling, and CORS.