APIs (Application Programming Interfaces) are the backbone of modern software development, enabling seamless communication between different applications or components.
However, like any technology, APIs are not immune to errors. In this brief guide, we’ll explore some common HTTP status codes and shed light on this crucial aspect of web API development with R Plumber.
Following the R way of handling errors, we will dive into the internals of error signaling, conditions, and handlers. Using this knowledge we will build on {plumber}’s error handling utilities to signal errors to the client in a tidy and flexible way from any point in the lifecycle of the request processing.
But first, let’s go over the most common HTTP status codes and explain what they mean.
Entirely new to Plumber? Check out our beginner-friendly guide to making REST APIs in R.
Common HTTP status codes encompass a range of issues spanning from request errors, such as Bad Request or Forbidden, to transient errors, like Service Unavailable, providing insight into the health and functionality of the system.
And now with this knowledge out of the way, let’s briefly discuss some other types of errors you’re likely to run into when building APIs with R Plumber.
API providers often impose rate limits to control the number of requests a client can make within a specific time frame. Transient errors are temporary glitches in the API’s functionality. They might occur due to network issues, server overloads, or momentary service disruptions.
The API should provide documentation for developers so that they can implement appropriate strategies, such as exponential backoff, to handle rate-limited scenarios gracefully.
Some common status code examples are:
You now know what these errors mean, but how can you handle them in R and R Plumber? That’s what we’ll discuss next.
You can run your R scripts or even R Plumber APIs in Docker – Follow our new guide to get started.
In R, unusual events are signaled and handled using conditions. These form a hierarchy with three main categories: errors, warnings, and messages. Errors indicate critical issues that typically halt the execution of code, warnings signify potential problems that don’t necessarily halt execution but should be addressed, and messages provide informative feedback without affecting code execution.
To understand the basics of error signaling in R, let’s focus on two base functions: simpleError()
and stop()
. These functions are used in the context of handling critical failures in the execution flow of a program, but they serve slightly different purposes:
simpleError()
by itself does not halt execution or signal the error condition to the R interpreter. It’s essentially a way to create a standardized error object that can then be used with stop() or other condition-handling mechanisms.stop()
function can take an error message directly, or it can take an error object created by simpleError()
. When stop()
is called, it halts the execution of the program and, unless caught by a condition handling construct like tryCatch()
, will terminate the execution of the current expression and propagate the error up the call stack.In the R Plumber framework, all errors originating from endpoints or filters are captured and passed to an error handler for processing as condition objects. This default handler responds with a status code of 500 (Internal Server Error).
However, we have the flexibility to implement our own error-handling function. This custom function allows us to examine the error condition metadata and decide on the most suitable response code to return to the client. Our goal is to manage errors gracefully and to deliver a clear and helpful error response to the client.
But how can we equip these error conditions with metadata?
In the R programming language, conditions can be equipped with metadata, providing additional context and information about the event. This metadata is particularly useful in the context of classed conditions, a concept rooted in R’s S3 system.
Eager to grasp the essence of S3 OOP in R? Don’t miss out on Part 2 of our series for simplified insights.
Classed conditions allow for the creation of custom condition classes, each tailored to represent a specific type of error or exceptional situation.
By assigning a class to a condition, developers can impart semantic meaning to the error, facilitating more targeted handling and interpretation. Leveraging this approach, we will standardize how we handle API error responses, ensuring uniformity and robustness in handling different categories of errors (494, 401, 503, etc.).
error_bad_argument <- errorCondition("Non-numeric input", class = "bad_argument") my_log <- function(x) { if(!is.numeric(x)) stop(error_bad_argument) log(x) } # Handle the ‘bad_argument’ classed error tryCatch( my_log("a"), bad_argument = function(cnd) "handled bad_argument", error = function(cnd) "other error" )
Or using {rlang} equivalently:
library(rlang) my_log <- function(x) { if(!is.numeric(x)) abort("Non-numeric input", class = "bad_argument") log(x) } # Handle the ‘bad_argument’ classed error: try_fetch( my_log("a"), bad_argument = function(cnd) "handled bad_argument", error = function(cnd) "other error" )
By executing the above code the classed error returned by my_log()
is handled by bad_argument
handler in try_fetch()
, returning:
Console Output Indicating Handled Exception
In the provided code snippet below, we demonstrate the creation of a custom handler for 404 Not Found errors. {plumber} treats this type of error separately using the dedicated function pr_set_404()
:
library(plumber) handler_404 <- function(req, res) { res$status <- 404 res$body <- "Can't find the requested resource" } # Create Plumber router pr() |> pr_get("/hi", function() "Hello") |> pr_set_404(handler_404) |> pr_run()
Now, if you visit the server URL of {plumber} requesting a resource that doesn’t exist, {plumber} will return a status code 404 with our custom message.
404 Not Found Error in Browser
pr_set_error()
The pr_set_error()
in {plumber} enables streamlined API error handling. By defining a custom error handler, such as handler_error()
in the snippet below, developers can effectively manage errors that arise during client requests. This handler specifies the HTTP status code and provides a concise error message, ensuring consistent and informative responses to clients.
library(plumber) handler_error <- function(req, res, err) { res$status <- 500 list(error = "Custom Error Message") } pr() |> pr_get("/error", function() 1 + "1") |> pr_set_error(handler_error) |> pr_run()
Now, if we request the “/error” resource, we’ll get the custom error message:
Internal Server Error Displayed in Browser
abort()
and pr_set_error()
{plumber} will catch any error raised in the endpoints and pass the associated condition object to the error handler. This approach simplifies error management by providing a single place for all the logic related to error handling. By using classed conditions in the endpoints, developers can easily determine the most appropriate response for each type of error encountered.
In the error handler, we can employ the try_fetch()
function to get an elegant solution for picking the appropriate HTTP error status based on the class of the condition. This approach enables the propagation of metadata about the condition that caused the error from any point in the lifecycle of the request processing, facilitating a tidy and uniform way to return HTTP status error codes (e.g., 400, 429, 500, etc.).
Example exercise: Create an endpoint that generates a status code 400 if a request parameter is missing, add an error message that the issue is on the client side, and attach metadata to the HTTP response about the missing parameter.
We can break down the answer into three parts:
library(plumber) library(rlang) hi_function <- function(name) { if(is_missing(name)) { abort("name parameter is missing", class = "error_400") } paste("Hi", name) }
name
parameter is missing; it aborts the request with a custom error message and class error_400
.name
parameter is provided, it concatenates “Hi” with the provided name
and returns the result.handler_error <- function(req, res, err) { # Transform vectors of length 1 to JSON strings res$serializer <- serializer_unboxed_json() try_fetch({ cnd_signal(err) }, error_400 = \(cnd) { res$status <- 400 list( error = "Server cannot process the request due to a client error", message = cnd_message(cnd) ) }, error = \(cnd) { res$status <- 500 list( error = "Oopsie: Internal Server Error" ) }) }
cnd_signal()
signals the error condition; try_fetch()
catches and passes it to the appropriate error handlers.error_400
, indicating a client error, the HTTP status code of the response is set to 400, and a JSON object containing an error message and the message associated with the error (cnd_message(e)
) is returned.error_400
handler), indicating an internal server error, the HTTP status code of the response is set to 500, and a generic error message is returned.pr() |> pr_get("/hi", hi_function) |> pr_set_error(handler_error) |> pr_run()
pr()
), defines a GET endpoint at “/hi” that is handled by the hi_function
, and sets the error handler for the router to handler_error
.hi_function
, and any errors that occur during request processing are handled by handler_error
.Now if we make a GET request without providing the name
parameter that is required, we will get a 400 status code followed by an informative message.
400 Bad Request Error for Missing Parameter in Web Browser
By utilizing this pattern, we can construct error handlers tailored to various HTTP status codes, such as 400 for client errors, 429 for rate limit exceeded errors, and 500 for internal server errors, allowing for comprehensive error management in our API endpoints. This approach enables precise control over error responses, ensuring the appropriate status code and message are returned to clients based on the encountered condition.
In conclusion, crafting solid APIs that return meaningful status codes is essential for enhancing user experience and facilitating effective communication between clients and servers.
By adhering to the R way of conditions, developers can implement robust error-handling mechanisms that seamlessly integrate with their APIs. Leveraging the flexibility of custom error classes and precise condition signaling, the R programming language provides a powerful framework for constructing APIs that deliver clear and informative error responses, ultimately fostering trust and reliability in application interactions.
Did you find this useful? Join Alexandros and a community of innovative R/Shiny developers at ShinyConf 2024. Don’t miss out on early bird registration – reserve your spot today!
The post appeared first on appsilon.com/blog/.