R Plumber: How to Craft Error Responses that Speak Fluent HTTP

Reading time:
time
min

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.

Table of Contents

Common HTTP Status Codes

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.

  1. Error 400 – Bad Request: This status code indicates that the server cannot process the request due to a client error. It might be caused by malformed requests, missing parameters, or invalid input.
  2. Error 401 – Unauthorized: The server understands the request, but the client must authenticate itself to get the requested response. In simpler terms, the user lacks the necessary credentials to access the resource.
  3. Error 403 – Forbidden: Similar to 401, but in this case, even with authentication, the client doesn’t have permission to access the requested resource. It’s a security measure to prevent unauthorized access.
  4. Error 404 – Not Found: One of the most recognizable errors, 404 indicates that the server can’t find the requested resource. This could be due to a mistyped URL or the resource being temporarily or permanently removed.
  5. Error 500 – Internal Server Error: A generic error message indicating that the server encountered an unexpected condition preventing it from fulfilling the request. This could be due to bugs in the server-side code or issues with the server’s configuration.

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.

Rate Limits and Transient Errors

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:

  1. Error 503 – Service Unavailable: The server is temporarily unable to handle the request, usually due to maintenance or overloading. This status code suggests that the service may become available again, and developers should implement retry mechanisms with appropriate back-off strategies to manage these temporary unavailability periods.
  2. Error 429 – Too Many Requests: This status code signals that the client has sent too many requests within a given time frame, exceeding the rate limit set by the API provider. Developers should implement strategies such as exponential backoff to handle rate-limited scenarios.

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.

The R Way: Best Practices for Error Handling in R Plumber

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() is a function that creates a condition object. This object represents an error state and includes information about the error, such as an error message and a class. However, 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 both creates an error condition and immediately signals it, which interrupts the normal flow of execution and exits from the current function or expression with an error. The 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.).

Classes Error Example


<pre><code>
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"
)
</code></pre>

Or using {rlang} equivalently:


<pre><code>
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"
)
</code></pre>

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

To delve deeper into the realm of conditions and to learn more about adding contextual information to errors you can find excellent examples in base R documentation and in {rlang}.


Error Handlers in R Plumber

Custom 404 Handler in {plumber}

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

Custom Error Handling with 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

Custom R Plumber Error Handling with 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:

1. Creating a function that signals the error:


library(plumber)
library(rlang)

hi_function <- function(name) {
  if(is_missing(name)) {
    abort("name parameter is missing", class = "error_400")
  }
  paste("Hi", name)
}
  • If the name parameter is missing; it aborts the request with a custom error message and class error_400.
  • If the name parameter is provided, it concatenates “Hi” with the provided name and returns the result.

2. Creating a custom error handler:


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.
  • If the error class is 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.
  • If the error class is unspecified (i.e., not caught by the 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.

3. Initialising plumber API:

pr() |>
  pr_get("/hi", hi_function) |>
  pr_set_error(handler_error) |>
  pr_run()


  • This sequence of chained function calls sets up the Plumber router (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.
  • Requests to the “/hi” endpoint are processed by 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

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.

Summing up R Plumber Custom Error Messages

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!

Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!

Is Your Software GxP Compliant?

Download a checklist designed for clinical managers in data departments to make sure that software meets requirements for FDA and EMA submissions.
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
best practices
r