Skip to content

Report errors from R APIs using "Problem Details" (RFC 7807)

License

Notifications You must be signed in to change notification settings

atheriel/httpproblems

Repository files navigation

httpproblems

CRAN status R-CMD-check

This R package is an answer to the question “how can I report errors from my R API in a standard, consistent way?”

Specifically, it implements the “Problem Details” specification from RFC 7807, which is an emerging standard for errors supported by many other languages and tools.

Although httpproblems originated for use with Plumber (see below), it has no dependencies of its own and can be used with any R web framework.

Installation

You can install the latest release of httpproblems from CRAN:

install.packages("httpproblems")

or the development version from GitHub with

# install.packages("remotes")
remotes::install_github("atheriel/httpproblems")

Usage

httpproblems provides helpers to create a few common HTTP errors:

  • bad_request()
  • not_found()
  • unauthorized()
  • forbidden()
  • conflict()
  • internal_server_error()

as well as a generic http_problem() that can be used with any valid status code.

These functions return a normal list with elements in line with the standard:

library(httpproblems)

body <- bad_request("Parameter 'id' must be a number.")
str(body)
#> List of 4
#>  $ type  : chr "https://tools.ietf.org/html/rfc7231#section-6.5.1"
#>  $ title : chr "Bad Request"
#>  $ status: int 400
#>  $ detail: chr "Parameter 'id' must be a number."
#>  - attr(*, "class")= chr [1:2] "http_problem" "list"

This object could be returned by an endpoint as normal after serialization to e.g. JSON.

In addition to these basic methods, httpproblems also has a set of stop_for_http_problem() functions:

  • stop_for_bad_request()
  • stop_for_not_found()
  • stop_for_unauthorized()
  • stop_for_forbidden()
  • stop_for_conflict()
  • stop_for_internal_server_error()

These leverage R’s extensible condition system to issue custom errors for these problems:

stop_for_bad_request("Parameter 'id' must be a number.")
#> Error: Bad Request (HTTP 400). Parameter 'id' must be a number.

# What does the error actually contain?
tryCatch(
  stop_for_bad_request("Parameter 'id' must be a number."),
  error = function(e) {
    str(e)
  }
)
#> List of 3
#>  $ message: chr "Bad Request (HTTP 400). Parameter 'id' must be a number."
#>  $ status : int 400
#>  $ body   :List of 4
#>   ..$ type  : chr "https://tools.ietf.org/html/rfc7231#section-6.5.1"
#>   ..$ title : chr "Bad Request"
#>   ..$ status: int 400
#>   ..$ detail: chr "Parameter 'id' must be a number."
#>   ..- attr(*, "class")= chr [1:2] "http_problem" "list"
#>  - attr(*, "class")= chr [1:4] "http_problem_error" "http_error" "error" "condition"

Typically, web frameworks allow you to control how uncaught errors are reported – which can be combined with these helpers to ensure that all API errors have a consistent format for your users.

Use with Plumber

Plumber allows users to control how errors are handled (via pr_set_error()), which we can employ to produce a Problem Details structure for both expected and unexpected errors:

library(plumber)

pr() %>%
  pr_get("/bad", function(req, res) {
    stop_for_bad_request("The 'id' parameter must be a number.")
  }) %>%
  pr_get("/bug", function(req, res) {
    stop("Another R error.")
  }) %>%
  pr_set_error(function(req, res, err) {
    # Force "unboxed" JSON and the Content-Type from RFC 7807.
    res$serializer <- serializer_unboxed_json(
      type = "application/problem+json"
    )
    # If we have an http_problem_error, use its status and body
    # fields. Otherwise, issue a 500 Internal Server Error.
    if (inherits(err, "http_problem_error")) {
      res$status <- err$status
      return(err$body)
    }
    if (isTRUE(req$pr$getDebug())) {
      internal_server_error(detail = err$message)
    } else {
      internal_server_error()
    }
  }) %>%
  pr_run(port = 4444)

Users interacting with this API will see the following:

$ curl -vs localhost:4444/bad
> GET /bad HTTP/1.1
> Host: localhost:4444
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
< Date: Tue, 09 Feb 2021 15:19:33 GMT
< Content-Type: application/problem+json
< Content-Length: 160
< 
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "The 'id' parameter must be a number."
}
$ curl -vs localhost:4444/bug
> GET /bug HTTP/1.1
> Host: localhost:4444
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 09 Feb 2021 15:19:59 GMT
< Content-Type: application/problem+json
< Content-Length: 150
< 
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Another R error."
}

You may also wish to use not_found() for Plumber’s 404 handler via pr_set_404().

License

Copyright 2021 Aaron Jacobs

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.