diff --git a/NEWS.md b/NEWS.md index 20441a6d..bb74701a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # httr2 (development version) +* `req_url_query()` gains the ability to control how spaces are encoded (#432). * New `resp_request()` aids debugging by returning the request associated with a response (#604). * `print.request()` now correctly escapes `{}` in headers (#586). * New `req_headers_redacted()` provides a user-friendlier way to set redacted headers (#561). diff --git a/R/req-url.R b/R/req-url.R index cc317dbc..5964532b 100644 --- a/R/req-url.R +++ b/R/req-url.R @@ -74,12 +74,17 @@ req_url_relative <- function(req, url) { #' If none of these options work for your needs, you can instead supply a #' function that takes a character vector of argument values and returns a #' a single string. +#' @param .space How should spaces in query params be escaped? The default, +#' "percent", uses standard percent encoding (i.e. `%20`), but you can opt-in +#' to "form" encoding, which uses `+` instead. req_url_query <- function(.req, ..., - .multi = c("error", "comma", "pipe", "explode")) { + .multi = c("error", "comma", "pipe", "explode"), + .space = c("percent", "form") + ) { check_request(.req) - dots <- multi_dots(..., .multi = .multi) + dots <- multi_dots(..., .multi = .multi, .space = .space) url <- url_parse(.req$url) url$query <- modify_list(url$query, !!!dots) diff --git a/R/url.R b/R/url.R index cd07befc..0c308f6d 100644 --- a/R/url.R +++ b/R/url.R @@ -249,6 +249,7 @@ elements_build <- function(x, name, collapse, error_call = caller_env()) { format_query_param <- function(x, name, multi = FALSE, + form = FALSE, error_call = caller_env()) { check_query_param(x, name, multi = multi, error_call = error_call) @@ -256,7 +257,11 @@ format_query_param <- function(x, unclass(x) } else { x <- format(x, scientific = FALSE, trim = TRUE, justify = "none") - curl::curl_escape(x) + x <- curl::curl_escape(x) + if (form) { + x <- gsub("%20", "+", x, fixed = TRUE) + } + x } } check_query_param <- function(x, name, multi = FALSE, error_call = caller_env()) { diff --git a/R/utils-multi.R b/R/utils-multi.R index aa16e353..3fd851fe 100644 --- a/R/utils-multi.R +++ b/R/utils-multi.R @@ -1,5 +1,6 @@ multi_dots <- function(..., .multi = c("error", "comma", "pipe", "explode"), + .space = c("percent", "form"), error_arg = "...", error_call = caller_env()) { if (is.function(.multi)) { @@ -7,6 +8,8 @@ multi_dots <- function(..., } else { .multi <- arg_match(.multi, error_arg = ".multi", error_call = error_call) } + .space <- arg_match(.space, call = error_call) + form <- .space == "form" dots <- list2(...) if (length(dots) == 0) { @@ -31,20 +34,20 @@ multi_dots <- function(..., n <- lengths(dots) if (any(n > 1)) { if (is.function(.multi)) { - dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE) + dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE, form = form) dots[n > 1] <- lapply(dots[n > 1], .multi) dots[n > 1] <- lapply(dots[n > 1], I) } else if (.multi == "comma") { - dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE) + dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE, form = form) dots[n > 1] <- lapply(dots[n > 1], paste0, collapse = ",") dots[n > 1] <- lapply(dots[n > 1], I) } else if (.multi == "pipe") { - dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE) + dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE, form = form) dots[n > 1] <- lapply(dots[n > 1], paste0, collapse = "|") dots[n > 1] <- lapply(dots[n > 1], I) } else if (.multi == "explode") { dots <- explode(dots) - dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE) + dots[n > 1] <- imap(dots[n > 1], format_query_param, multi = TRUE, form = form) dots[n > 1] <- lapply(dots[n > 1], I) } else if (.multi == "error") { cli::cli_abort( @@ -58,7 +61,12 @@ multi_dots <- function(..., } # Format other params - dots[n == 1] <- imap(dots[n == 1], format_query_param, error_call = error_call) + dots[n == 1] <- imap( + dots[n == 1], + format_query_param, + form = form, + error_call = error_call + ) dots[n == 1] <- lapply(dots[n == 1], I) dots diff --git a/man/req_url.Rd b/man/req_url.Rd index 933e7a58..a213a4ff 100644 --- a/man/req_url.Rd +++ b/man/req_url.Rd @@ -12,7 +12,12 @@ req_url(req, url) req_url_relative(req, url) -req_url_query(.req, ..., .multi = c("error", "comma", "pipe", "explode")) +req_url_query( + .req, + ..., + .multi = c("error", "comma", "pipe", "explode"), + .space = c("percent", "form") +) req_url_path(req, ...) @@ -43,6 +48,10 @@ containing multiple values: If none of these options work for your needs, you can instead supply a function that takes a character vector of argument values and returns a a single string.} + +\item{.space}{How should spaces in query params be escaped? The default, +"percent", uses standard percent encoding (i.e. \verb{\%20}), but you can opt-in +to "form" encoding, which uses \code{+} instead.} } \value{ A modified HTTP \link{request}. diff --git a/tests/testthat/_snaps/req-url.md b/tests/testthat/_snaps/req-url.md index aeb5062a..90c05484 100644 --- a/tests/testthat/_snaps/req-url.md +++ b/tests/testthat/_snaps/req-url.md @@ -1,3 +1,11 @@ +# can control space handling + + Code + req_url_query(req, a = " ", .space = "bar") + Condition + Error in `multi_dots()`: + ! `.space` must be one of "percent" or "form", not "bar". + # can handle multi query params Code diff --git a/tests/testthat/test-req-url.R b/tests/testthat/test-req-url.R index a76ecfa8..759c33a8 100644 --- a/tests/testthat/test-req-url.R +++ b/tests/testthat/test-req-url.R @@ -52,6 +52,17 @@ test_that("can set query params", { expect_equal(req_url_query(req, !!!list(a = 1, a = 2))$url, "http://example.com/?a=1&a=2") }) +test_that("can control space handling", { + req <- request("http://example.com/") + expect_equal(req_url_query(req, a = " ")$url, "http://example.com/?a=%20") + expect_equal(req_url_query(req, a = " ", .space = "form")$url, "http://example.com/?a=+") + + expect_snapshot( + req_url_query(req, a = " ", .space = "bar"), + error = TRUE + ) +}) + test_that("can handle multi query params", { req <- request("http://example.com/")