From 0e3ca898d1e46c191a4153bbb578b272d7a809f7 Mon Sep 17 00:00:00 2001 From: Zac Davies Date: Fri, 10 Jul 2026 23:07:04 +1000 Subject: [PATCH 1/3] Allow configurable OAuth expiry margins Expose `expiry_margin` across request-based OAuth helpers and increase the default safety margin from 5 to 30 seconds. Preserve the normal refresh-token path while allowing clients to accommodate servers that reject near-expiry tokens. Fixes #860. --- NEWS.md | 1 + R/oauth-flow-auth-code.R | 14 ++++- R/oauth-flow-client-credentials.R | 11 +++- R/oauth-flow-device.R | 11 +++- R/oauth-flow-jwt.R | 11 +++- R/oauth-flow-password.R | 11 +++- R/oauth-flow-refresh.R | 11 +++- R/oauth-flow-token-exchange.R | 11 +++- R/oauth-token.R | 2 +- R/oauth.R | 33 +++++++++--- man/req_oauth.Rd | 6 ++- man/req_oauth_auth_code.Rd | 7 ++- man/req_oauth_bearer_jwt.Rd | 7 ++- man/req_oauth_client_credentials.Rd | 12 ++++- man/req_oauth_device.Rd | 7 ++- man/req_oauth_password.Rd | 7 ++- man/req_oauth_refresh.Rd | 7 ++- man/req_oauth_token_exchange.Rd | 7 ++- tests/testthat/_snaps/oauth.md | 8 +++ .../test-oauth-flow-client-credentials.R | 10 ++++ tests/testthat/test-oauth-token.R | 6 +-- tests/testthat/test-oauth.R | 52 +++++++++++++++++++ 22 files changed, 220 insertions(+), 32 deletions(-) create mode 100644 tests/testthat/test-oauth-flow-client-credentials.R diff --git a/NEWS.md b/NEWS.md index c688be302..9c43ee4c9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,7 @@ * `req_body_form()` and `req_url_query()` no longer error with "C stack usage is too close to the limit" when given very long string values (#805). * `req_cache()` no longer errors when a request is first performed with `path` then later without it (#840). * `req_error()` is now applied to responses retrieved from the cache, so a custom `is_error` callback is respected on cache hits (#806). +* `req_oauth_*()` gains an `expiry_margin` argument to control how early cached OAuth tokens are treated as expired; the default margin increases from 5 to 30 seconds (@zacdav-db, #860). * `req_oauth_bearer_jwt()` now uses its `claim` as the basis for a separate client assertion when the `client` also authenticates with `auth = "jwt_sig"`, so you no longer need to supply the claim twice. As a result, `oauth_client(auth = "jwt_sig")` no longer requires a `claim` in `auth_params` at creation time (#825). * `req_oauth_device()` gains a `pkce` argument to enable Proof Key for Code Exchange, matching `oauth_flow_device()` (#834). * `req_throttle()` can now enforce multiple rate limits at once: supply a vector to `capacity` (and `fill_time_s`) to create one token bucket per limit, and each request must satisfy all of them (#555). diff --git a/R/oauth-flow-auth-code.R b/R/oauth-flow-auth-code.R index 43d9e7c6f..6e5aceeea 100644 --- a/R/oauth-flow-auth-code.R +++ b/R/oauth-flow-auth-code.R @@ -83,6 +83,9 @@ #' Learn more in . #' @param cache_key If you want to cache multiple tokens per app, use this #' key to disambiguate them. +#' @param expiry_margin Number of seconds before a token's stated expiry that +#' it should be treated as expired. Increase this for servers that reject +#' tokens shortly before they expire. Defaults to 30 seconds. #' @returns `req_oauth_auth_code()` returns a modified HTTP [request] that will #' use OAuth; `oauth_flow_auth_code()` returns an [oauth_token]. #' @examples @@ -106,7 +109,8 @@ req_oauth_auth_code <- function( token_params = list(), redirect_uri = oauth_redirect_uri(), cache_disk = FALSE, - cache_key = NULL + cache_key = NULL, + expiry_margin = 30 ) { auth_url <- oauth_flow_url(auth_url, client, "auth_url") redirect <- normalize_redirect_uri(redirect_uri = redirect_uri) @@ -122,7 +126,13 @@ req_oauth_auth_code <- function( ) cache <- cache_choose(client, cache_disk, cache_key) - req_oauth(req, "oauth_flow_auth_code", params, cache = cache) + req_oauth( + req, + "oauth_flow_auth_code", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-flow-client-credentials.R b/R/oauth-flow-client-credentials.R index 9a6bfe740..ed9a5766d 100644 --- a/R/oauth-flow-client-credentials.R +++ b/R/oauth-flow-client-credentials.R @@ -28,7 +28,8 @@ req_oauth_client_credentials <- function( req, client, scope = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) { params <- list( client = client, @@ -37,7 +38,13 @@ req_oauth_client_credentials <- function( ) cache <- cache_mem(client, NULL) - req_oauth(req, "oauth_flow_client_credentials", params, cache = cache) + req_oauth( + req, + "oauth_flow_client_credentials", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-flow-device.R b/R/oauth-flow-device.R index 8977c3865..289a63ccf 100644 --- a/R/oauth-flow-device.R +++ b/R/oauth-flow-device.R @@ -41,7 +41,8 @@ req_oauth_device <- function( auth_params = list(), token_params = list(), cache_disk = FALSE, - cache_key = NULL + cache_key = NULL, + expiry_margin = 30 ) { auth_url <- oauth_flow_url(auth_url, client, "device_auth_url") params <- list( @@ -54,7 +55,13 @@ req_oauth_device <- function( token_params = token_params ) cache <- cache_choose(client, cache_disk, cache_key) - req_oauth(req, "oauth_flow_device", params, cache = cache) + req_oauth( + req, + "oauth_flow_device", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-flow-jwt.R b/R/oauth-flow-jwt.R index af9002b07..ea3f063a8 100644 --- a/R/oauth-flow-jwt.R +++ b/R/oauth-flow-jwt.R @@ -46,7 +46,8 @@ req_oauth_bearer_jwt <- function( signature = "jwt_encode_sig", signature_params = list(), scope = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) { params <- list( client = client, @@ -58,7 +59,13 @@ req_oauth_bearer_jwt <- function( ) cache <- cache_mem(client, claim) - req_oauth(req, "oauth_flow_bearer_jwt", params, cache = cache) + req_oauth( + req, + "oauth_flow_bearer_jwt", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-flow-password.R b/R/oauth-flow-password.R index 1d28b143a..9afdc60d8 100644 --- a/R/oauth-flow-password.R +++ b/R/oauth-flow-password.R @@ -33,7 +33,8 @@ req_oauth_password <- function( scope = NULL, token_params = list(), cache_disk = FALSE, - cache_key = username + cache_key = username, + expiry_margin = 30 ) { params <- list( client = client, @@ -43,7 +44,13 @@ req_oauth_password <- function( token_params = token_params ) cache <- cache_choose(client, cache_disk = cache_disk, cache_key = cache_key) - req_oauth(req, "oauth_flow_password", params, cache = cache) + req_oauth( + req, + "oauth_flow_password", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-flow-refresh.R b/R/oauth-flow-refresh.R index c3d7d8ff1..73e8f9683 100644 --- a/R/oauth-flow-refresh.R +++ b/R/oauth-flow-refresh.R @@ -36,7 +36,8 @@ req_oauth_refresh <- function( client, refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"), scope = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) { params <- list( client = client, @@ -46,7 +47,13 @@ req_oauth_refresh <- function( ) cache <- cache_mem(client, refresh_token) - req_oauth(req, "oauth_flow_refresh", params, cache = cache) + req_oauth( + req, + "oauth_flow_refresh", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-flow-token-exchange.R b/R/oauth-flow-token-exchange.R index 7858ec133..e646431cb 100644 --- a/R/oauth-flow-token-exchange.R +++ b/R/oauth-flow-token-exchange.R @@ -65,7 +65,8 @@ req_oauth_token_exchange <- function( requested_token_type = NULL, actor_token = NULL, actor_token_type = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) { params <- list( client = client, @@ -80,7 +81,13 @@ req_oauth_token_exchange <- function( token_params = token_params ) cache <- cache_mem(client, NULL) - req_oauth(req, "oauth_flow_token_exchange", params, cache = cache) + req_oauth( + req, + "oauth_flow_token_exchange", + params, + cache = cache, + expiry_margin = expiry_margin + ) } #' @export diff --git a/R/oauth-token.R b/R/oauth-token.R index 05e9d07fb..e54ae37bd 100644 --- a/R/oauth-token.R +++ b/R/oauth-token.R @@ -66,7 +66,7 @@ print.httr2_token <- function(x, ...) { invisible(x) } -token_has_expired <- function(token, delay = 5) { +token_has_expired <- function(token, delay = 30) { if (is.null(token$expires_at)) { FALSE } else { diff --git a/R/oauth.R b/R/oauth.R index 16fa6759d..35cc2cc1a 100644 --- a/R/oauth.R +++ b/R/oauth.R @@ -15,36 +15,57 @@ #' @param flow An `oauth_flow_` function used to generate the access token. #' @param flow_params Parameters for the flow. This should be a named list #' whose names match the argument names of `flow`. +#' @param expiry_margin Number of seconds before a token's stated expiry that +#' it should be treated as expired. Increase this for servers that reject +#' tokens shortly before they expire. Defaults to 30 seconds. #' @returns An [oauth_token]. #' @keywords internal #' @export -req_oauth <- function(req, flow, flow_params, cache) { +req_oauth <- function(req, flow, flow_params, cache, expiry_margin = 30) { + check_number_whole(expiry_margin, min = 0) + # Want req object to contain meaningful objects, not just a closure req <- req_auth_sign( req, fun = auth_oauth_sign, - params = list(flow = flow, flow_params = flow_params), + params = list( + flow = flow, + flow_params = flow_params, + expiry_margin = expiry_margin + ), cache = cache ) req <- req_policies(req, auth_oauth = TRUE) req } -auth_oauth_sign <- function(req, cache, flow, flow_params) { +auth_oauth_sign <- function( + req, + cache, + flow, + flow_params, + expiry_margin = 30 +) { token <- auth_oauth_token_get( cache = cache, flow = flow, - flow_params = flow_params + flow_params = flow_params, + expiry_margin = expiry_margin ) req_auth_bearer_token(req, token$access_token) } -auth_oauth_token_get <- function(cache, flow, flow_params = list()) { +auth_oauth_token_get <- function( + cache, + flow, + flow_params = list(), + expiry_margin = 30 +) { token <- cache$get() if (is.null(token)) { token <- exec(flow, !!!flow_params) cache$set(token) - } else if (token_has_expired(token)) { + } else if (token_has_expired(token, delay = expiry_margin)) { cache$clear() if (is.null(token$refresh_token)) { token <- exec(flow, !!!flow_params) diff --git a/man/req_oauth.Rd b/man/req_oauth.Rd index cc8872eae..19d3be32e 100644 --- a/man/req_oauth.Rd +++ b/man/req_oauth.Rd @@ -4,7 +4,7 @@ \alias{req_oauth} \title{OAuth authentication} \usage{ -req_oauth(req, flow, flow_params, cache) +req_oauth(req, flow, flow_params, cache, expiry_margin = 30) } \arguments{ \item{req}{A httr2 \link{request} object.} @@ -22,6 +22,10 @@ cached yet. \item \code{set()} saves the token to the cache. \item \code{clear()} removes the token from the cache }} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ An \link{oauth_token}. diff --git a/man/req_oauth_auth_code.Rd b/man/req_oauth_auth_code.Rd index 98f65cec6..76509feee 100644 --- a/man/req_oauth_auth_code.Rd +++ b/man/req_oauth_auth_code.Rd @@ -15,7 +15,8 @@ req_oauth_auth_code( token_params = list(), redirect_uri = oauth_redirect_uri(), cache_disk = FALSE, - cache_key = NULL + cache_key = NULL, + expiry_margin = 30 ) oauth_flow_auth_code( @@ -81,6 +82,10 @@ Learn more in \url{https://httr2.r-lib.org/articles/oauth.html}.} \item{cache_key}{If you want to cache multiple tokens per app, use this key to disambiguate them.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_auth_code()} returns a modified HTTP \link{request} that will diff --git a/man/req_oauth_bearer_jwt.Rd b/man/req_oauth_bearer_jwt.Rd index 0d6b6ee79..66fac3b98 100644 --- a/man/req_oauth_bearer_jwt.Rd +++ b/man/req_oauth_bearer_jwt.Rd @@ -12,7 +12,8 @@ req_oauth_bearer_jwt( signature = "jwt_encode_sig", signature_params = list(), scope = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) oauth_flow_bearer_jwt( @@ -49,6 +50,10 @@ supplies its own \code{claim} in \code{auth_params}. The client assertion claim \item{token_params}{List containing additional parameters passed to the \code{token_url}.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_bearer_jwt()} returns a modified HTTP \link{request} that will diff --git a/man/req_oauth_client_credentials.Rd b/man/req_oauth_client_credentials.Rd index c246bd5c9..3a71c4dc2 100644 --- a/man/req_oauth_client_credentials.Rd +++ b/man/req_oauth_client_credentials.Rd @@ -5,7 +5,13 @@ \alias{oauth_flow_client_credentials} \title{OAuth with client credentials} \usage{ -req_oauth_client_credentials(req, client, scope = NULL, token_params = list()) +req_oauth_client_credentials( + req, + client, + scope = NULL, + token_params = list(), + expiry_margin = 30 +) oauth_flow_client_credentials(client, scope = NULL, token_params = list()) } @@ -18,6 +24,10 @@ oauth_flow_client_credentials(client, scope = NULL, token_params = list()) \item{token_params}{List containing additional parameters passed to the \code{token_url}.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_client_credentials()} returns a modified HTTP \link{request} that will diff --git a/man/req_oauth_device.Rd b/man/req_oauth_device.Rd index 6d847492a..28458b166 100644 --- a/man/req_oauth_device.Rd +++ b/man/req_oauth_device.Rd @@ -15,7 +15,8 @@ req_oauth_device( auth_params = list(), token_params = list(), cache_disk = FALSE, - cache_key = NULL + cache_key = NULL, + expiry_margin = 30 ) oauth_flow_device( @@ -60,6 +61,10 @@ Learn more in \url{https://httr2.r-lib.org/articles/oauth.html}.} \item{cache_key}{If you want to cache multiple tokens per app, use this key to disambiguate them.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_device()} returns a modified HTTP \link{request} that will diff --git a/man/req_oauth_password.Rd b/man/req_oauth_password.Rd index 13dc5814b..47ab1a316 100644 --- a/man/req_oauth_password.Rd +++ b/man/req_oauth_password.Rd @@ -13,7 +13,8 @@ req_oauth_password( scope = NULL, token_params = list(), cache_disk = FALSE, - cache_key = username + cache_key = username, + expiry_margin = 30 ) oauth_flow_password( @@ -49,6 +50,10 @@ Learn more in \url{https://httr2.r-lib.org/articles/oauth.html}.} \item{cache_key}{If you want to cache multiple tokens per app, use this key to disambiguate them.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_password()} returns a modified HTTP \link{request} that will diff --git a/man/req_oauth_refresh.Rd b/man/req_oauth_refresh.Rd index bab7307ad..6e208c4a4 100644 --- a/man/req_oauth_refresh.Rd +++ b/man/req_oauth_refresh.Rd @@ -10,7 +10,8 @@ req_oauth_refresh( client, refresh_token = Sys.getenv("HTTR2_REFRESH_TOKEN"), scope = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) oauth_flow_refresh( @@ -34,6 +35,10 @@ is to look in \code{HTTR2_REFRESH_TOKEN}.} \item{token_params}{List containing additional parameters passed to the \code{token_url}.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_refresh()} returns a modified HTTP \link{request} that will diff --git a/man/req_oauth_token_exchange.Rd b/man/req_oauth_token_exchange.Rd index 0cad1df79..9c9f9a05a 100644 --- a/man/req_oauth_token_exchange.Rd +++ b/man/req_oauth_token_exchange.Rd @@ -16,7 +16,8 @@ req_oauth_token_exchange( requested_token_type = NULL, actor_token = NULL, actor_token_type = NULL, - token_params = list() + token_params = list(), + expiry_margin = 30 ) oauth_flow_token_exchange( @@ -65,6 +66,10 @@ one of the options in \href{https://datatracker.ietf.org/doc/html/rfc8693#sectio \item{token_params}{List containing additional parameters passed to the \code{token_url}.} + +\item{expiry_margin}{Number of seconds before a token's stated expiry that +it should be treated as expired. Increase this for servers that reject +tokens shortly before they expire. Defaults to 30 seconds.} } \value{ \code{req_oauth_token_exchange()} returns a modified HTTP \link{request} that diff --git a/tests/testthat/_snaps/oauth.md b/tests/testthat/_snaps/oauth.md index ec147c052..489c0d9f0 100644 --- a/tests/testthat/_snaps/oauth.md +++ b/tests/testthat/_snaps/oauth.md @@ -1,3 +1,11 @@ +# req_oauth validates and stores expiry margin + + Code + req_oauth(req, "", list(), NULL, expiry_margin = -1) + Condition + Error in `req_oauth()`: + ! `expiry_margin` must be a whole number larger than or equal to 0, not the number -1. + # can store on disk Code diff --git a/tests/testthat/test-oauth-flow-client-credentials.R b/tests/testthat/test-oauth-flow-client-credentials.R new file mode 100644 index 000000000..825b0724c --- /dev/null +++ b/tests/testthat/test-oauth-flow-client-credentials.R @@ -0,0 +1,10 @@ +test_that("can configure token expiry margin", { + client <- oauth_client("test", "https://example.com/token", secret = "secret") + default_req <- request("https://example.com") |> + req_oauth_client_credentials(client) + req <- request("https://example.com") |> + req_oauth_client_credentials(client, expiry_margin = 40) + + expect_equal(default_req$policies$auth_sign$params$expiry_margin, 30) + expect_equal(req$policies$auth_sign$params$expiry_margin, 40) +}) diff --git a/tests/testthat/test-oauth-token.R b/tests/testthat/test-oauth-token.R index 7d7fa9395..e883c8ea4 100644 --- a/tests/testthat/test-oauth-token.R +++ b/tests/testthat/test-oauth-token.R @@ -22,10 +22,10 @@ test_that("can compute token expiry", { token <- oauth_token("xyz") expect_equal(token_has_expired(token), FALSE) - # Respects delay - token <- oauth_token("xyz", expires_in = 8, .date = Sys.time() - 10) + token <- oauth_token("xyz", expires_in = 20, .date = Sys.time()) expect_equal(token_has_expired(token), TRUE) + expect_equal(token_has_expired(token, delay = 0), FALSE) - token <- oauth_token("xyz", expires_in = 10, .date = Sys.time()) + token <- oauth_token("xyz", expires_in = 60, .date = Sys.time()) expect_equal(token_has_expired(token), FALSE) }) diff --git a/tests/testthat/test-oauth.R b/tests/testthat/test-oauth.R index 943e5ee9a..ea8b8853c 100644 --- a/tests/testthat/test-oauth.R +++ b/tests/testthat/test-oauth.R @@ -101,6 +101,58 @@ test_that("can retrieve non-expired token from cache", { expect_equal(auth_oauth_token_get(cache, oauth_flow_refresh), token) }) +test_that("expiry margin controls when cached tokens are refreshed", { + client <- oauth_client("test", "http://example.org/test") + cache <- cache_mem(client) + cached <- oauth_token("cached", expires_in = 60) + refreshed <- oauth_token("refreshed") + cache$set(cached) + + expect_equal( + auth_oauth_token_get(cache, function(...) refreshed, expiry_margin = 30), + cached + ) + expect_equal( + auth_oauth_token_get(cache, function(...) refreshed, expiry_margin = 90), + refreshed + ) + expect_equal(cache$get(), refreshed) +}) + +test_that("expiry margin preserves refresh token behavior", { + client <- oauth_client("test", "http://example.org/test") + cache <- cache_mem(client) + cache$set( + oauth_token("cached", refresh_token = "refresh", expires_in = 60) + ) + + local_mocked_bindings( + token_refresh = function(client, refresh_token, token_params = list()) { + oauth_token("refreshed", used_refresh_token = refresh_token) + } + ) + + token <- auth_oauth_token_get( + cache, + function(...) NULL, + flow_params = list(client = client), + expiry_margin = 90 + ) + expect_equal(token$access_token, "refreshed") + expect_equal(token$used_refresh_token, "refresh") +}) + +test_that("req_oauth validates and stores expiry margin", { + req <- request("https://example.com") + req <- req_oauth(req, "", list(), NULL, expiry_margin = 40) + + expect_equal(req$policies$auth_sign$params$expiry_margin, 40) + expect_snapshot( + req_oauth(req, "", list(), NULL, expiry_margin = -1), + error = TRUE + ) +}) + # Cache ------------------------------------------------------------------- From 6f7b4151549087c659cea1b6109558818b4ed046 Mon Sep 17 00:00:00 2001 From: Zac Davies Date: Fri, 10 Jul 2026 23:27:47 +1000 Subject: [PATCH 2/3] Update snapshots for rlang 1.3.0 Accept the hash changes produced by rlang 1.3.0 in the current CI environment. --- tests/testthat/_snaps/oauth-client.md | 6 +++--- tests/testthat/_snaps/oauth.md | 2 +- tests/testthat/_snaps/req-cache.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/testthat/_snaps/oauth-client.md b/tests/testthat/_snaps/oauth-client.md index 030f86389..051ad2ab4 100644 --- a/tests/testthat/_snaps/oauth-client.md +++ b/tests/testthat/_snaps/oauth-client.md @@ -41,7 +41,7 @@ oauth_client("x", url) Output - * name : "bf27508f7925b06bf28a10f3805351ab" + * name : "9758a2659d8a24b1be8a873ab9e4da84" * id : "x" * token_url: "http://example.com" * auth : "oauth_client_req_auth_body" @@ -49,7 +49,7 @@ oauth_client("x", url, secret = "SECRET") Output - * name : "bf27508f7925b06bf28a10f3805351ab" + * name : "9758a2659d8a24b1be8a873ab9e4da84" * id : "x" * secret : * token_url: "http://example.com" @@ -60,7 +60,7 @@ }) Output - * name : "bf27508f7925b06bf28a10f3805351ab" + * name : "9758a2659d8a24b1be8a873ab9e4da84" * id : "x" * token_url: "http://example.com" * auth : diff --git a/tests/testthat/_snaps/oauth.md b/tests/testthat/_snaps/oauth.md index 489c0d9f0..5661d90d8 100644 --- a/tests/testthat/_snaps/oauth.md +++ b/tests/testthat/_snaps/oauth.md @@ -11,5 +11,5 @@ Code cache$set(1) Message - Caching httr2 token in '/httr2-test/ae743e0fbd718c21f2cca632e77bd180-token.rds.enc'. + Caching httr2 token in '/httr2-test/2c0a8a99dc147d5445c3b49d035665b2-token.rds.enc'. diff --git a/tests/testthat/_snaps/req-cache.md b/tests/testthat/_snaps/req-cache.md index 23fc83d3c..4129541dc 100644 --- a/tests/testthat/_snaps/req-cache.md +++ b/tests/testthat/_snaps/req-cache.md @@ -8,11 +8,11 @@ Code invisible(cache_post_fetch(req, resp)) Message - Saving response to cache "f3805db63ff822b4743f247cfdde10a3" + Saving response to cache "076ab9e79f298c15d4444b2eb6fffa84" Code invisible(cache_pre_fetch(req)) Message - Found url in cache "f3805db63ff822b4743f247cfdde10a3" + Found url in cache "076ab9e79f298c15d4444b2eb6fffa84" Cached value is fresh; using response from cache --- @@ -22,7 +22,7 @@ invisible(cache_pre_fetch(req)) Message Pruning cache - Found url in cache "f3805db63ff822b4743f247cfdde10a3" + Found url in cache "076ab9e79f298c15d4444b2eb6fffa84" Cached value is stale; checking for updates Code invisible(cache_post_fetch(req, response(304))) From f5554cffbe7de21d848ba9505427deb47c69c710 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Fri, 10 Jul 2026 16:16:04 -0500 Subject: [PATCH 3/3] Relocate news bullet --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 60488ca12..7694ce961 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ * Fixed OAuth token cache pruning so that it actually matches the encrypted `.rds.enc` files written to disk; previously the pruning pattern only matched an unencrypted `.rds` file that was never created, so cached tokens were never automatically deleted regardless of age. * httr2 now requires rlang >= 1.3.0, which changes the hash used to name files cached by `req_cache()` and on-disk OAuth token caches (e.g. from `req_oauth_auth_code(cache_disk = TRUE)`). Existing cached files won't match the new hash, so they'll be silently ignored (triggering a normal cache miss/re-authentication) and cleaned up over time by the usual pruning rules; you can also delete them manually. +* `req_oauth_*()` gains an `expiry_margin` argument to control how early cached OAuth tokens are treated as expired; the default margin increases from 5 to 30 seconds (@zacdav-db, #860). # httr2 1.2.3 @@ -17,7 +18,6 @@ * `req_body_form()` and `req_url_query()` no longer error with "C stack usage is too close to the limit" when given very long string values (#805). * `req_cache()` no longer errors when a request is first performed with `path` then later without it (#840). * `req_error()` is now applied to responses retrieved from the cache, so a custom `is_error` callback is respected on cache hits (#806). -* `req_oauth_*()` gains an `expiry_margin` argument to control how early cached OAuth tokens are treated as expired; the default margin increases from 5 to 30 seconds (@zacdav-db, #860). * `req_oauth_bearer_jwt()` now uses its `claim` as the basis for a separate client assertion when the `client` also authenticates with `auth = "jwt_sig"`, so you no longer need to supply the claim twice. As a result, `oauth_client(auth = "jwt_sig")` no longer requires a `claim` in `auth_params` at creation time (#825). * `req_oauth_device()` gains a `pkce` argument to enable Proof Key for Code Exchange, matching `oauth_flow_device()` (#834). * `req_throttle()` can now enforce multiple rate limits at once: supply a vector to `capacity` (and `fill_time_s`) to create one token bucket per limit, and each request must satisfy all of them (#555).