diff --git a/R/iterate-helpers.R b/R/iterate-helpers.R index a81c963a..95a55a99 100644 --- a/R/iterate-helpers.R +++ b/R/iterate-helpers.R @@ -47,7 +47,7 @@ #' # will generate a better progress bar. #' #' resps <- req_perform_iterative( -#' req %>% req_url_query(limit = 1), +#' req |> req_url_query(limit = 1), #' next_req = iterate_with_offset( #' "page_index", #' resp_pages = function(resp) resp_body_json(resp)$pages diff --git a/R/iterate.R b/R/iterate.R index 0eb4665b..6c57687f 100644 --- a/R/iterate.R +++ b/R/iterate.R @@ -25,7 +25,7 @@ #' ```R #' next_req <- function(resp, req) { #' cursor <- resp_body_json(resp)$next_cursor -#' req %>% req_body_json_modify(cursor = cursor) +#' req |> req_body_json_modify(cursor = cursor) #' } #' ``` #' @@ -39,7 +39,7 @@ #' cursor <- resp_body_json(resp)$next_cursor #' if (is.null(cursor)) #' return(NULL) -#' req %>% req_body_json_modify(cursor = cursor) +#' req |> req_body_json_modify(cursor = cursor) #' } #' ``` #' @@ -61,7 +61,7 @@ #' return(NULL) #' #' signal_total_pages(body$pages) -#' req %>% req_body_json_modify(cursor = cursor) +#' req |> req_body_json_modify(cursor = cursor) #' } #' ``` #' diff --git a/R/sequential.R b/R/sequential.R index 40fcdb36..9b16e72c 100644 --- a/R/sequential.R +++ b/R/sequential.R @@ -22,11 +22,11 @@ #' chunks <- unname(split(ids, (seq_along(ids) - 1) %/% 10)) #' #' # Then we use lapply to generate one request for each chunk: -#' reqs <- chunks %>% lapply(\(idx) req %>% req_url_query(id = idx, .multi = "comma")) +#' reqs <- chunks |> lapply(\(idx) req |> req_url_query(id = idx, .multi = "comma")) #' #' # Then we can perform them all and get the results #' \dontrun{ -#' resps <- reqs %>% req_perform_sequential() +#' resps <- reqs |> req_perform_sequential() #' resps_data(resps, \(resp) resp_body_json(resp)) #' } req_perform_sequential <- function(reqs, diff --git a/man/iterate_with_offset.Rd b/man/iterate_with_offset.Rd index 7d8d386d..25cdf711 100644 --- a/man/iterate_with_offset.Rd +++ b/man/iterate_with_offset.Rd @@ -77,7 +77,7 @@ resps <- req_perform_iterative( # will generate a better progress bar. resps <- req_perform_iterative( - req \%>\% req_url_query(limit = 1), + req |> req_url_query(limit = 1), next_req = iterate_with_offset( "page_index", resp_pages = function(resp) resp_body_json(resp)$pages diff --git a/man/req_perform_iterative.Rd b/man/req_perform_iterative.Rd index 540af804..95c46ca6 100644 --- a/man/req_perform_iterative.Rd +++ b/man/req_perform_iterative.Rd @@ -18,7 +18,7 @@ req_perform_iterative( \item{next_req}{A function that takes the previous response (\code{resp}) and request (\code{req}) and returns a \link{request} for the next page or \code{NULL} if -the iteration should terminate.} +the iteration should terminate. See below for more details.} \item{path}{Optionally, path to save the body of request. This should be a glue string that uses \code{{i}} to distinguish different requests. @@ -69,7 +69,7 @@ the request. The simplest version of this function might look like this: \if{html}{\out{
}}\preformatted{next_req <- function(resp, req) \{ cursor <- resp_body_json(resp)$next_cursor - req \%>\% req_body_json_modify(cursor = cursor) + req |> req_body_json_modify(cursor = cursor) \} }\if{html}{\out{
}} @@ -82,7 +82,7 @@ returning \code{NULL}: cursor <- resp_body_json(resp)$next_cursor if (is.null(cursor)) return(NULL) - req \%>\% req_body_json_modify(cursor = cursor) + req |> req_body_json_modify(cursor = cursor) \} }\if{html}{\out{}} @@ -103,7 +103,7 @@ like this: return(NULL) signal_total_pages(body$pages) - req \%>\% req_body_json_modify(cursor = cursor) + req |> req_body_json_modify(cursor = cursor) \} }\if{html}{\out{}} } diff --git a/man/req_perform_sequential.Rd b/man/req_perform_sequential.Rd index 6a3b0547..a5752d80 100644 --- a/man/req_perform_sequential.Rd +++ b/man/req_perform_sequential.Rd @@ -58,11 +58,11 @@ ids <- sort(sample(100, 50)) chunks <- unname(split(ids, (seq_along(ids) - 1) \%/\% 10)) # Then we use lapply to generate one request for each chunk: -reqs <- chunks \%>\% lapply(\(idx) req \%>\% req_url_query(id = idx, .multi = "comma")) +reqs <- chunks |> lapply(\(idx) req |> req_url_query(id = idx, .multi = "comma")) # Then we can perform them all and get the results \dontrun{ -resps <- reqs \%>\% req_perform_sequential() +resps <- reqs |> req_perform_sequential() resps_data(resps, \(resp) resp_body_json(resp)) } } diff --git a/vignettes/articles/oauth.Rmd b/vignettes/articles/oauth.Rmd index 8ea79ab8..6635184c 100644 --- a/vignettes/articles/oauth.Rmd +++ b/vignettes/articles/oauth.Rmd @@ -200,7 +200,7 @@ If we make a request to this endpoint without authentication, we'll get an error ```{r} #| error: true req <- request("https://api.github.com/user") -req %>% +req |> req_perform() ``` @@ -208,13 +208,13 @@ We can authenticate this request with `req_oauth_auth_code()`, using the same ar ```{r} #| eval: false -req %>% +req |> req_oauth_auth_code( client = github_client(), auth_url = "https://github.com/login/oauth/authorize" - ) %>% - req_perform() %>% - resp_body_json() %>% + ) |> + req_perform() |> + resp_body_json() |> str() ``` diff --git a/vignettes/articles/wrapping-apis.Rmd b/vignettes/articles/wrapping-apis.Rmd index b4931080..7f163e44 100644 --- a/vignettes/articles/wrapping-apis.Rmd +++ b/vignettes/articles/wrapping-apis.Rmd @@ -36,15 +36,15 @@ library(httr2) ```{r, include = FALSE} # Seems to return 500s from time-to-time, so avoid any problems # by only evaluating other chunks if a simple request succeeds. -faker_status_images <- request("https://fakerapi.it/api/v1") %>% - req_url_path_append("images") %>% - req_error(is_error = ~ FALSE) %>% - req_perform() %>% +faker_status_images <- request("https://fakerapi.it/api/v1") |> + req_url_path_append("images") |> + req_error(is_error = ~ FALSE) |> + req_perform() |> resp_status() -faker_status_persons <- request("https://fakerapi.it/api/v1") %>% - req_url_path_append("persons") %>% - req_error(is_error = ~ FALSE) %>% - req_perform() %>% +faker_status_persons <- request("https://fakerapi.it/api/v1") |> + req_url_path_append("persons") |> + req_error(is_error = ~ FALSE) |> + req_perform() |> resp_status() faker_ok <- faker_status_images < 400 && faker_status_persons < 400 @@ -56,15 +56,15 @@ Before we start writing the sort of functions that you might put in a package, w ```{r, eval = faker_ok} # We start by creating a request that uses the base API url req <- request("https://fakerapi.it/api/v1") -resp <- req %>% +resp <- req |> # Then we add on the images path - req_url_path_append("images") %>% + req_url_path_append("images") |> # Add query parameters _width and _quantity - req_url_query(`_width` = 380, `_quantity` = 1) %>% + req_url_query(`_width` = 380, `_quantity` = 1) |> req_perform() # The result comes back as JSON -resp %>% resp_body_json() %>% str() +resp |> resp_body_json() |> str() ``` ### Errors @@ -73,8 +73,8 @@ It's always worth a little early experimentation to see if we get any useful inf The httr2 defaults get in your way here, because if you retrieve an unsuccessful HTTP response, you automatically get an error that prevents you from further inspecting the body: ```{r, error = TRUE, eval = faker_ok} -req %>% - req_url_path_append("invalid") %>% +req |> + req_url_path_append("invalid") |> req_perform() ``` @@ -82,14 +82,14 @@ However, you can access the last response (successful or not) with `last_respons ```{r, eval = faker_ok} resp <- last_response() -resp %>% resp_body_json() +resp |> resp_body_json() ``` It doesn't look like there's anything useful there. Sometimes useful info is returned in the headers, so let's check: ```{r, eval = faker_ok} -resp %>% resp_headers() +resp |> resp_headers() ``` It doesn't look like we're getting any more useful information, so we can leave the `req_error()` default as is. @@ -101,8 +101,8 @@ If you're wrapping this code into a package, it's considered polite to set a use You can do this with the `req_user_agent()` function: ```{r, eval = faker_ok} -req %>% - req_user_agent("my_package_name (http://my.package.web.site)") %>% +req |> + req_user_agent("my_package_name (http://my.package.web.site)") |> req_dry_run() ``` @@ -131,11 +131,11 @@ faker <- function(resource, ..., quantity = 1, locale = "en_US", seed = NULL) { ) names(params) <- paste0("_", names(params)) - request("https://fakerapi.it/api/v1") %>% - req_url_path_append(resource) %>% - req_url_query(!!!params) %>% - req_user_agent("my_package_name (http://my.package.web.site)") %>% - req_perform() %>% + request("https://fakerapi.it/api/v1") |> + req_url_path_append(resource) |> + req_url_query(!!!params) |> + req_user_agent("my_package_name (http://my.package.web.site)") |> + req_perform() |> resp_body_json() } @@ -270,7 +270,9 @@ In this section, I'll show you how to store that key so that you (and your autom httr2 is built around the notion that this key should live in an environment variable. So the first step is to make your package key available on your local development machine by adding a line to your your user-level `.Renviron` (which you can easily open with `usethis::edit_r_environ()`): - YOURPACKAGE_KEY=key_you_generated_with_secret_make_key +``` +YOURPACKAGE_KEY=key_you_generated_with_secret_make_key +``` ```{r, include = FALSE} Sys.setenv(YOURPACKAGE_KEY = secret_make_key()) @@ -351,9 +353,9 @@ And indeed, you'll see that in the examples below --- this is bad practice for a Now let's perform a test request and look at the response: ```{r} -resp <- request("https://api.nytimes.com/svc/books/v3") %>% - req_url_path_append("/reviews.json") %>% - req_url_query(`api-key` = my_key, isbn = 9780307476463) %>% +resp <- request("https://api.nytimes.com/svc/books/v3") |> + req_url_path_append("/reviews.json") |> + req_url_query(`api-key` = my_key, isbn = 9780307476463) |> req_perform() resp ``` @@ -361,8 +363,8 @@ resp Like most modern APIs, this one returns the results as JSON: ```{r} -resp %>% - resp_body_json() %>% +resp |> + resp_body_json() |> str() ``` @@ -374,9 +376,9 @@ What happens if there's an error? For example, if we deliberately supply an invalid key: ```{r, error = TRUE} -resp <- request("https://api.nytimes.com/svc/books/v3") %>% - req_url_path_append("/reviews.json") %>% - req_url_query(`api-key` = "invalid", isbn = 9780307476463) %>% +resp <- request("https://api.nytimes.com/svc/books/v3") |> + req_url_path_append("/reviews.json") |> + req_url_query(`api-key` = "invalid", isbn = 9780307476463) |> req_perform() ``` @@ -385,13 +387,13 @@ To see if there's any extra useful information we can again look at `last_respon ```{r} resp <- last_response() resp -resp %>% resp_body_json() +resp |> resp_body_json() ``` It looks like there's some useful additional info in the `faultstring`: ```{r} -resp %>% resp_body_json() %>% .$fault %>% .$faultstring +resp |> resp_body_json() |> .$fault |> .$faultstring ``` To add that information to future errors we can use the `body` argument to `req_error()`. @@ -400,13 +402,13 @@ Once we do that and re-fetch the request, we see the additional information disp ```{r, error = TRUE} nytimes_error_body <- function(resp) { - resp %>% resp_body_json() %>% .$fault %>% .$faultstring + resp |> resp_body_json() |> .$fault |> .$faultstring } -resp <- request("https://api.nytimes.com/svc/books/v3") %>% - req_url_path_append("/reviews.json") %>% - req_url_query(`api-key` = "invalid", isbn = 9780307476463) %>% - req_error(body = nytimes_error_body) %>% +resp <- request("https://api.nytimes.com/svc/books/v3") |> + req_url_path_append("/reviews.json") |> + req_url_query(`api-key` = "invalid", isbn = 9780307476463) |> + req_error(body = nytimes_error_body) |> req_perform() ``` @@ -425,9 +427,9 @@ That means we can't use `req_retry()`, which automatically waits the amount of t Instead, we'll use `req_throttle()` to ensure we don't make more than 10 requests every 60 seconds: ```{r} -req <- request("https://api.nytimes.com/svc/books/v3") %>% - req_url_path_append("/reviews.json") %>% - req_url_query(`api-key` = "invalid", isbn = 9780307476463) %>% +req <- request("https://api.nytimes.com/svc/books/v3") |> + req_url_path_append("/reviews.json") |> + req_url_query(`api-key` = "invalid", isbn = 9780307476463) |> req_throttle(10 / 60) ``` @@ -435,9 +437,9 @@ By default, `req_throttle()` shares the limit across all requests made to the ho Since the docs suggest the rate limit applies per API, you might want to use the `realm` argument to be a bit more specific: ```{r} -req <- request("https://api.nytimes.com/svc/books/v3") %>% - req_url_path_append("/reviews.json") %>% - req_url_query(`api-key` = "invalid", isbn = 9780307476463) %>% +req <- request("https://api.nytimes.com/svc/books/v3") |> + req_url_path_append("/reviews.json") |> + req_url_query(`api-key` = "invalid", isbn = 9780307476463) |> req_throttle(10 / 60, realm = "https://api.nytimes.com/svc/books") ``` @@ -447,12 +449,12 @@ Putting together all the pieces above yields a function something like this: ```{r} nytimes_books <- function(api_key, path, ...) { - request("https://api.nytimes.com/svc/books/v3") %>% - req_url_path_append("/reviews.json") %>% - req_url_query(..., `api-key` = api_key) %>% - req_error(body = nytimes_error_body) %>% - req_throttle(10 / 60, realm = "https://api.nytimes.com/svc/books") %>% - req_perform() %>% + request("https://api.nytimes.com/svc/books/v3") |> + req_url_path_append("/reviews.json") |> + req_url_query(..., `api-key` = api_key) |> + req_error(body = nytimes_error_body) |> + req_throttle(10 / 60, realm = "https://api.nytimes.com/svc/books") |> + req_perform() |> resp_body_json() } @@ -557,10 +559,10 @@ It's also a good idea to give every token a descriptive name, that reminds you o To authenticate a request with the token, we need to put it in the `Authorization` header with a ["token" prefix](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#authentication): ```{r} -req <- request("https://api.github.com/gists") %>% +req <- request("https://api.github.com/gists") |> req_headers(Authorization = paste("token", token)) -req %>% req_perform() +req |> req_perform() ``` Because the authorization header usually contains secret information, httr2 automatically redacts it[^1]: @@ -570,7 +572,7 @@ Because the authorization header usually contains secret information, httr2 auto ```{r} req -req %>% req_dry_run() +req |> req_dry_run() ``` ### Errors @@ -583,9 +585,9 @@ You'll typically need to tackle your error handling iteratively, improving your While GitHub does [document its errors](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors), I'm sufficiently distrustful that I still want to construct a deliberately malformed query and see what happens: ```{r, error = TRUE} -resp <- request("https://api.github.com/gists") %>% - req_url_query(since = "abcdef") %>% - req_headers(Authorization = paste("token", token)) %>% +resp <- request("https://api.github.com/gists") |> + req_url_query(since = "abcdef") |> + req_headers(Authorization = paste("token", token)) |> req_perform() ``` @@ -595,7 +597,7 @@ But the response is rather different to documentation which suggests there shoul ```{r} resp <- last_response() resp -resp %>% resp_body_json() +resp |> resp_body_json() ``` I'll proceed anyway, writing a function that extracts the data and formats it for presentation to the user: @@ -616,10 +618,10 @@ gist_error_body(resp) Now I can pass this function to the `body` argument of `req_error()` and it will be automatically included in the error when a request fails: ```{r, error = TRUE} -request("https://api.github.com/gists") %>% - req_url_query(since = "yesterday") %>% - req_headers(Authorization = paste("token", token)) %>% - req_error(body = gist_error_body) %>% +request("https://api.github.com/gists") |> + req_url_query(since = "yesterday") |> + req_headers(Authorization = paste("token", token)) |> + req_error(body = gist_error_body) |> req_perform() ``` @@ -631,8 +633,8 @@ While we're thinking about errors, it's useful to look at what happens if the re Luckily, GitHub consistently uses response headers to provide information about the remaining [rate limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). ```{r} -resp <- req %>% req_perform() -resp %>% resp_headers("ratelimit") +resp <- req |> req_perform() +resp |> resp_headers("ratelimit") ``` We can teach httr2 about this so it can automatically wait for a reset if the rate limit is hit. @@ -663,7 +665,7 @@ gist_after(resp) We then pass functions to `req_retry()` so httr2 has all the information it needs to handle rate-limiting automatically: ```{r} -request("http://api.github.com") %>% +request("http://api.github.com") |> req_retry( is_transient = gist_is_transient, after = gist_after, @@ -679,9 +681,9 @@ Let's wrap up everything we've learned so far into a single function that create ```{r} req_gist <- function(token) { - request("https://api.github.com/gists") %>% - req_headers(Authorization = paste("token", token)) %>% - req_error(body = gist_error_body) %>% + request("https://api.github.com/gists") |> + req_headers(Authorization = paste("token", token)) |> + req_error(body = gist_error_body) |> req_retry( is_transient = gist_is_transient, after = gist_after @@ -689,7 +691,7 @@ req_gist <- function(token) { } # Check it works: -req_gist(token) %>% +req_gist(token) |> req_perform() ``` @@ -701,13 +703,13 @@ To [create a gist](https://docs.github.com/en/rest/reference/gists#create-a-gist httr2 provides one function that does both of these things: `req_body_json()`: ```{r} -req <- req_gist(token) %>% +req <- req_gist(token) |> req_body_json(list( description = "This is my cool gist!", files = list(test.R = list(content = "print('Hi!')")), public = FALSE )) -req %>% req_dry_run() +req |> req_dry_run() ``` Depending on the API you're wrapping, you might need to send data in a different way. @@ -718,8 +720,8 @@ Typically, the API will return some useful data about the resource you've just c Here I'll extract the gist ID so we can use it in the next examples, culminating with deleting the gist so I don't end up with a bunch of duplicated gists 😃. ```{r} -resp <- req %>% req_perform() -id <- resp %>% resp_body_json() %>% .$id +resp <- req |> req_perform() +id <- resp |> resp_body_json() |> .$id id ``` @@ -730,11 +732,11 @@ To do so, I need to again send JSON encoded data, but this time I need to use th So after adding the data to request, I use `req_method()` to override the default method: ```{r} -req <- req_gist(token) %>% - req_url_path_append(id) %>% - req_body_json(list(description = "This is a simple gist")) %>% +req <- req_gist(token) |> + req_url_path_append(id) |> + req_body_json(list(description = "This is a simple gist")) |> req_method("PATCH") -req %>% req_dry_run() +req |> req_dry_run() ``` ### Deleting a gist @@ -742,9 +744,9 @@ req %>% req_dry_run() Deleting a gist is similar, except we don't send any data, we just need to adjust the default method from `GET` to `DELETE`. ```{r} -req <- req_gist(token) %>% - req_url_path_append(id) %>% +req <- req_gist(token) |> + req_url_path_append(id) |> req_method("DELETE") -req %>% req_dry_run() -req %>% req_perform() +req |> req_dry_run() +req |> req_perform() ``` diff --git a/vignettes/httr2.Rmd b/vignettes/httr2.Rmd index dc51932b..b2e678d1 100644 --- a/vignettes/httr2.Rmd +++ b/vignettes/httr2.Rmd @@ -8,9 +8,12 @@ vignette: > --- ```{r, include = FALSE} +has_pipe <- getRversion() >= "4.1.0" + knitr::opts_chunk$set( collapse = TRUE, - comment = "#>" + comment = "#>", + eval = has_pipe ) ``` @@ -20,6 +23,7 @@ httr2 is designed to map closely to the underlying HTTP protocol, which I'll exp For more details, I also recommend "[An overview of HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview)" from MDN. ```{r setup} +#| eval: true library(httr2) ``` @@ -41,7 +45,13 @@ Here, instead of an external website, we use a test server that's built-in to ht We can see exactly what this request will send to the server with a dry run: ```{r} -req %>% req_dry_run() +req |> req_dry_run() +``` + +```{r} +#| include: FALSE +#| eval: true +port <- if (has_pipe) paste0("`", url_parse(example_url())$port, "`") else "e.g. `1234`" ``` The first line of the request contains three important pieces of information: @@ -50,7 +60,7 @@ The first line of the request contains three important pieces of information: Here's its GET, the most common verb, indicating that we want to *get* a resource. Other verbs include POST, to create a new resource, PUT, to replace an existing resource, and DELETE, to delete a resource. -- The **path**, which is the URL stripped of details that the server already knows, i.e. the protocol (`http` or `https`), the host (`localhost`), and the port (`r url_parse(example_url())$port`). +- The **path**, which is the URL stripped of details that the server already knows, i.e. the protocol (`http` or `https`), the host (`localhost`), and the port (`r port`). - The version of the HTTP protocol. This is unimportant for our purposes because it's handled at a lower level. @@ -59,12 +69,12 @@ The following lines specify the HTTP **headers**, a series of name-value pairs s The headers in this request were automatically added by httr2, but you can override them or add your own with `req_headers()`: ```{r} -req %>% +req |> req_headers( Name = "Hadley", `Shoe-Size` = "11", Accept = "application/json" - ) %>% + ) |> req_dry_run() ``` @@ -76,8 +86,8 @@ The `req_body_*()` functions provide a variety of ways to add data to the body. Here we'll use `req_body_json()` to add some data encoded as JSON: ```{r} -req %>% - req_body_json(list(x = 1, y = "a")) %>% +req |> + req_body_json(list(x = 1, y = "a")) |> req_dry_run() ``` @@ -96,16 +106,16 @@ Different servers want data encoded differently so httr2 provides a selection of For example, `req_body_form()` uses the encoding used when you submit a form from a web browser: ```{r} -req %>% - req_body_form(x = "1", y = "a") %>% +req |> + req_body_form(x = "1", y = "a") |> req_dry_run() ``` And `req_body_multipart()` uses the multipart encoding which is particularly important when you need to send larger amounts of data or complete files: ```{r} -req %>% - req_body_multipart(x = "1", y = "a") %>% +req |> + req_body_multipart(x = "1", y = "a") |> req_dry_run() ``` @@ -116,15 +126,15 @@ If you need to send data encoded in a different form, you can use `req_body_raw( To actually perform a request and fetch the response back from the server, call `req_perform()`: ```{r} -req <- request(example_url()) %>% req_url_path("/json") -resp <- req %>% req_perform() +req <- request(example_url()) |> req_url_path("/json") +resp <- req |> req_perform() resp ``` You can see a simulation of what httr2 actually received with `resp_raw()`: ```{r} -resp %>% resp_raw() +resp |> resp_raw() ``` An HTTP response has a very similar structure to an HTTP request. @@ -137,40 +147,40 @@ You can extract data from the response using the `resp_()` functions: - `resp_status()` returns the status code and `resp_status_desc()` returns the description: ```{r} - resp %>% resp_status() - resp %>% resp_status_desc() + resp |> resp_status() + resp |> resp_status_desc() ``` - You can extract all headers with `resp_headers()` or a specific header with `resp_header()`: ```{r} - resp %>% resp_headers() - resp %>% resp_header("Content-Length") + resp |> resp_headers() + resp |> resp_header("Content-Length") ``` Headers are case insensitive: ```{r} - resp %>% resp_header("ConTEnT-LeNgTH") + resp |> resp_header("ConTEnT-LeNgTH") ``` - You can extract the body in various forms using the `resp_body_*()` family of functions. Since this response returns JSON we can use `resp_body_json()`: ```{r} - resp %>% resp_body_json() %>% str() + resp |> resp_body_json() |> str() ``` Responses with status codes 4xx and 5xx are HTTP errors. httr2 automatically turns these into R errors: ```{r, error = TRUE} -request(example_url()) %>% - req_url_path("/status/404") %>% +request(example_url()) |> + req_url_path("/status/404") |> req_perform() -request(example_url()) %>% - req_url_path("/status/500") %>% +request(example_url()) |> + req_url_path("/status/500") |> req_perform() ```