diff --git a/NAMESPACE b/NAMESPACE index e2f4ff2a..112fc391 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -60,6 +60,7 @@ export(req_body_multipart) export(req_body_raw) export(req_cache) export(req_cookie_preserve) +export(req_cookies_set) export(req_dry_run) export(req_error) export(req_headers) diff --git a/NEWS.md b/NEWS.md index 076e94d4..1f14699b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # httr2 (development version) +* New `req_cookie_set()` allows you to set client side cookies (#369). * `req_body_file()` no longer leaks a connection if the response doesn't complete succesfully (#534). * `req_perform()` no longer displays a progress bar when sleeping during tests. You can override this behaviour by setting the option `httr2_progress`. * `req_cache()` now re-caches the response if the body is hasn't been modified but the headers have changed (#442). diff --git a/R/req-cookies.R b/R/req-cookies.R index 29291f4b..65e13159 100644 --- a/R/req-cookies.R +++ b/R/req-cookies.R @@ -1,37 +1,68 @@ -#' Preserve cookies across requests +#' Set and preserve cookies +#' +#' @description +#' Use `req_cookie_set()` to set client side cookies that are sent to the +#' server. #' #' By default, httr2 uses a clean slate for every request meaning that cookies -#' are not automatically preserved across requests. To preserve cookies, you -#' must set a cookie file which will be read before and updated after each -#' request. +#' are not automatically preserved across requests. To preserve cookies, use +#' `req_cookie_preserve()` along with the path to cookie file that will be +#' read before and updated after each request. #' #' @inheritParams req_perform #' @param path A path to a file where cookies will be read from before and updated after the request. #' @export #' @examples +#' # Use `req_cookies_set()` to set client-side cookies +#' request(example_url()) |> +#' req_cookies_set(a = 1, b = 1) |> +#' req_dry_run() +#' +#' # Use `req_cookie_preserve()` to preserve server-side cookies across requests #' path <- tempfile() -#' httpbin <- request(example_url()) |> -#' req_cookie_preserve(path) #' -#' # Manually set two cookies -#' httpbin |> +#' # Set a server-side cookie +#' request(example_url()) |> +#' req_cookie_preserve(path) |> #' req_template("/cookies/set/:name/:value", name = "chocolate", value = "chip") |> #' req_perform() |> #' resp_body_json() #' -#' httpbin |> +#' # Set another sever-side cookie +#' request(example_url()) |> +#' req_cookie_preserve(path) |> #' req_template("/cookies/set/:name/:value", name = "oatmeal", value = "raisin") |> #' req_perform() |> #' resp_body_json() #' +#' # Add a client side cookie +#' request(example_url()) |> +#' req_url_path("/cookies/set") |> +#' req_cookie_preserve(path) |> +#' req_cookies_set(snicker = "doodle") |> +#' req_perform() |> +#' resp_body_json() +#' #' # The cookie path has a straightforward format #' cat(readChar(path, nchars = 1e4)) req_cookie_preserve <- function(req, path) { check_request(req) check_string(path, allow_empty = FALSE) - req_options(req, - cookiejar = path, - cookiefile = path - ) + req_options(req, cookiejar = path, cookiefile = path) +} + +#' @export +#' @rdname req_cookie_preserve +#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> +#' Name-value pairs that define query parameters. Each value must be +#' an atomic vector, which is automatically escaped. To opt-out of escaping, +#' wrap strings in `I()`. +req_cookies_set <- function(req, ...) { + check_request(req) + req_options(req, cookie = cookies_build(list2(...))) +} + +cookies_build <- function(x, error_call = caller_env()) { + elements_build(x, "Cookies", ";", error_call = error_call) } diff --git a/R/url.R b/R/url.R index b4f3cf0c..8ffab225 100644 --- a/R/url.R +++ b/R/url.R @@ -165,8 +165,12 @@ query_parse <- function(x) { } query_build <- function(x, error_call = caller_env()) { + elements_build(x, "Query", "&", error_call = error_call) +} + +elements_build <- function(x, name, collapse, error_call = caller_env()) { if (!is_list(x) || (!is_named(x) && length(x) > 0)) { - cli::cli_abort("Query must be a named list.", call = error_call) + cli::cli_abort("{name} must be a named list.", call = error_call) } x <- compact(x) @@ -177,7 +181,7 @@ query_build <- function(x, error_call = caller_env()) { values <- map2_chr(x, names(x), format_query_param, error_call = error_call) names <- curl::curl_escape(names(x)) - paste0(names, "=", values, collapse = "&") + paste0(names, "=", values, collapse = collapse) } format_query_param <- function(x, diff --git a/man/req_cookie_preserve.Rd b/man/req_cookie_preserve.Rd index 5bbcb23b..0ef9f237 100644 --- a/man/req_cookie_preserve.Rd +++ b/man/req_cookie_preserve.Rd @@ -2,37 +2,63 @@ % Please edit documentation in R/req-cookies.R \name{req_cookie_preserve} \alias{req_cookie_preserve} -\title{Preserve cookies across requests} +\alias{req_cookies_set} +\title{Set and preserve cookies} \usage{ req_cookie_preserve(req, path) + +req_cookies_set(req, ...) } \arguments{ \item{req}{A httr2 \link{request} object.} \item{path}{A path to a file where cookies will be read from before and updated after the request.} + +\item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> +Name-value pairs that define query parameters. Each value must be +an atomic vector, which is automatically escaped. To opt-out of escaping, +wrap strings in \code{I()}.} } \description{ +Use \code{req_cookie_set()} to set client side cookies that are sent to the +server. + By default, httr2 uses a clean slate for every request meaning that cookies -are not automatically preserved across requests. To preserve cookies, you -must set a cookie file which will be read before and updated after each -request. +are not automatically preserved across requests. To preserve cookies, use +\code{req_cookie_preserve()} along with the path to cookie file that will be +read before and updated after each request. } \examples{ +# Use `req_cookies_set()` to set client-side cookies +request(example_url()) |> + req_cookies_set(a = 1, b = 1) |> + req_dry_run() + +# Use `req_cookie_preserve()` to preserve server-side cookies across requests path <- tempfile() -httpbin <- request(example_url()) |> - req_cookie_preserve(path) -# Manually set two cookies -httpbin |> +# Set a server-side cookie +request(example_url()) |> + req_cookie_preserve(path) |> req_template("/cookies/set/:name/:value", name = "chocolate", value = "chip") |> req_perform() |> resp_body_json() -httpbin |> +# Set another sever-side cookie +request(example_url()) |> + req_cookie_preserve(path) |> req_template("/cookies/set/:name/:value", name = "oatmeal", value = "raisin") |> req_perform() |> resp_body_json() +# Add a client side cookie +request(example_url()) |> + req_url_path("/cookies/set") |> + req_cookie_preserve(path) |> + req_cookies_set(snicker = "doodle") |> + req_perform() |> + resp_body_json() + # The cookie path has a straightforward format cat(readChar(path, nchars = 1e4)) } diff --git a/tests/testthat/test-req-cookies.R b/tests/testthat/test-req-cookies.R index 039362af..85d3cda4 100644 --- a/tests/testthat/test-req-cookies.R +++ b/tests/testthat/test-req-cookies.R @@ -20,3 +20,21 @@ test_that("can read/write cookies", { expect_mapequal(cookies, list(x = "a", y = "b", z = "c")) }) + +test_that("can set cookies", { + resp <- request(example_url()) %>% + req_cookies_set(a = 1, b = 1) %>% + req_url_path("/cookies") %>% + req_perform() + + expect_equal(resp_body_json(resp), list(cookies = list(a = "1", b = "1"))) +}) + +test_that("cookie values are usually escaped", { + resp <- request(example_url()) %>% + req_cookies_set(a = I("%20"), b = "%") %>% + req_url_path("/cookies") %>% + req_perform() + + expect_equal(resp_body_json(resp), list(cookies = list(a = "%20", b = "%25"))) +})