Skip to content

Commit

Permalink
Control how spaces are handed in query strings (#609)
Browse files Browse the repository at this point in the history
Fixes #432
  • Loading branch information
hadley authored Jan 6, 2025
1 parent 63b6e14 commit e485d7f
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 9 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
9 changes: 7 additions & 2 deletions R/req-url.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion R/url.R
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,19 @@ 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)

if (inherits(x, "AsIs")) {
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()) {
Expand Down
18 changes: 13 additions & 5 deletions R/utils-multi.R
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
multi_dots <- function(...,
.multi = c("error", "comma", "pipe", "explode"),
.space = c("percent", "form"),
error_arg = "...",
error_call = caller_env()) {
if (is.function(.multi)) {
check_function2(.multi, call = error_call, arg = ".multi")
} 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) {
Expand All @@ -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(
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion man/req_url.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/testthat/_snaps/req-url.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 11 additions & 0 deletions tests/testthat/test-req-url.R
Original file line number Diff line number Diff line change
Expand Up @@ -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/")

Expand Down

0 comments on commit e485d7f

Please sign in to comment.