HTTP Requests with httr2

· 4 min read · Updated March 11, 2026 · intermediate
r http api httr2 web

The httr2 package is the modern way to make HTTP requests in R. It provides a pipe-friendly interface for working with HTTP verbs, handling authentication, and processing API responses. Whether you’re consuming REST APIs, downloading datasets, or integrating with web services, httr2 gives you the tools to do it reliably.

Installation and Setup

Install httr2 from CRAN:

install.packages("httr2")
library(httr2)

The package is part of the tidyverse ecosystem but works independently. It requires R 4.1 or later for the native pipe operator support, though you can use the magrittr pipe if on an older version.

Making Your First Request

The core function is request(). Create a request object, then modify it with methods:

library(httr2)

# Create a request
req <- request("https://httpbin.org/get")

# Execute the request
resp <- req |> req_perform()

# Check the status
resp |> resp_status()
# [1] 200

# Get the body as text
resp |> resp_body_string()

Each method returns the modified request object, enabling the pipe chain. This makes it easy to build up complex requests step by step.

HTTP Methods

GET Requests

GET requests retrieve data. Pass query parameters with req_url_query():

request("https://httpbin.org/get") |>
  req_url_query(name = "Alice", age = 30) |>
  req_perform() |>
  resp_body_json()

You can chain multiple query parameters. They get encoded automatically.

POST Requests

POST requests send data to create or update resources. Use req_body_json() for JSON payloads:

request("https://httpbin.org/post") |>
  req_method("POST") |>
  req_body_json(list(message = "Hello API", priority = "high")) |>
  req_perform() |>
  resp_body_json()

For form data, use req_body_form():

request("https://httpbin.org/post") |>
  req_body_form(name = "Alice", submit = "Register") |>
  req_perform()

Other HTTP Verbs

# PUT - complete replacement
request("https://api.example.com/resource/1") |>
  req_method("PUT") |>
  req_body_json(list(updated = TRUE, data = "new content")) |>
  req_perform()

# DELETE - remove a resource
request("https://api.example.com/resource/1") |>
  req_method("DELETE") |>
  req_perform()

# PATCH - partial update
request("https://api.example.com/resource/1") |>
  req_method("PATCH") |>
  req_body_json(list(status = "active")) |>
  req_perform()

Handling Headers

Add custom headers with req_headers(). This is essential for API authentication and content negotiation:

request("https://api.github.com/user") |>
  req_headers(
    Authorization = paste("Bearer", Sys.getenv("GITHUB_TOKEN")),
    Accept = "application/vnd.github.v3+json",
    `User-Agent` = "MyRApp/1.0"
  ) |>
  req_perform() |>
  resp_body_json()

Common headers include Authorization tokens, Accept types for content negotiation, and User-Agent strings to identify your application.

Authentication

Bearer Tokens

request("https://api.example.com/protected") |>
  req_auth_bearer("your-token-here") |>
  req_perform()

Bearer tokens are commonly used in modern REST APIs, especially with OAuth 2.0 implementations.

Basic Authentication

request("https://api.example.com/login") |>
  req_auth_basic("username", "password") |>
  req_perform()

Basic auth sends credentials encoded in Base64. It is simple but less secure for sensitive applications.

OAuth 2.0

httr2 supports OAuth 2.0 authorization code flow:

# Create OAuth client configuration
oauth <- oauth_client(
  id = "your-client-id",
  token_url = "https://provider.com/oauth/token",
  secret = "your-client-secret"
)

# Get authorization token
token <- oauth |>
  oauth_flow_auth_code("https://provider.com/authorize")

# Use token in subsequent requests
request("https://api.example.com/data") |>
  req_auth_bearer(token$access_token) |>
  req_perform()

This handles token refresh automatically when supported by the provider.

Error Handling

Use req_error() to control how errors are handled:

# Stop on any HTTP error
request("https://httpbin.org/status/404") |>
  req_error(is_error = ~ paste("HTTP error:", .x$status_code)) |>
  req_perform()

# Handle errors gracefully with possibly
safe_request <- possibly(req_perform, otherwise = NULL)

resp <- request("https://httpbin.org/status/500") |>
  req_error(is_error = \(resp) FALSE) |>
  safe_request()

if (is.null(resp)) {
  cat("Request failed\n")
} else {
  cat("Request succeeded:", resp_status(resp), "\n")
}

Working with Responses

Parse JSON

resp <- request("https://httpbin.org/json") |> 
  req_perform()

# Parse as R list
data <- resp |> resp_body_json()

# Access nested elements
data$slideshow$title

Parse XML

resp <- request("https://httpbin.org/xml") |>
  req_perform()

# Parse with xml2
library(xml2)
doc <- resp |> resp_body_xml()
xml_find_all(doc, ".//title")

Download Files

# Download binary file
request("https://httpbin.org/bytes/1024") |>
  req_save("download.bin")

# Download with progress
request("https://httpbin.org/bytes/1024000") |>
  req_save("large.bin", progress = TRUE)

Rate Limiting

Many APIs impose rate limits. Use req_throttle() to space out requests:

# Wait 1 second between requests
req <- request("https://api.example.com/items") |>
  req_throttle(1)

# Iterate through pages with rate limiting
for (page in 1:10) {
  resp <- request("https://api.example.com/items") |>
    req_url_query(page = page) |>
    req_throttle(1) |>
    req_perform()
  
  # Process response
  items <- resp |> resp_body_json()
  cat("Page", page, "-", length(items), "items\n")
}

Retry Logic

Handle transient failures with req_retry():

request("https://httpbin.org/delay/2") |>
  req_retry(
    max_tries = 3,
    max_seconds = 10,
    is_transient = ~ .x$status_code >= 500
  ) |>
  req_perform()

The is_transient function determines which status codes warrant a retry.

Practical Example: GitHub API

# Fetch user information from GitHub
github_user <- function(username) {
  request(paste0("https://api.github.com/users/", username)) |>
    req_headers(
      Accept = "application/vnd.github.v3+json",
      `User-Agent` = "R-httr2-demo"
    ) |>
    req_perform() |>
    resp_body_json()
}

# Get repository information
github_repos <- function(username, limit = 30) {
  request(paste0("https://api.github.com/users/", username, "/repos")) |>
    req_headers(
      Accept = "application/vnd.github.v3+json",
      `User-Agent` = "R-httr2-demo"
    ) |>
    req_url_query(per_page = min(limit, 100)) |>
    req_perform() |>
    resp_body_json()
  
    tibble::tibble(
      name = sapply(response, function(x) x$name),
      stars = sapply(response, function(x) x$stargazers_count),
      url = sapply(response, function(x) x$html_url)
    )
}

# Usage
user <- github_user("hadley")
cat("Name:", user$name, "\n")
cat("Public repos:", user$public_repos, "\n")
cat("Followers:", user$followers, "\n")

repos <- github_repos("hadley", 10)
print(head(repos, 5))

See Also