Skip to content

Commit 56f9c8b

Browse files
authored
Redact headers in str() (#692)
Fixes #682
1 parent db76035 commit 56f9c8b

20 files changed

+143
-62
lines changed

NAMESPACE

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ S3method("$",httr2_headers)
44
S3method("[",httr2_headers)
55
S3method("[[",httr2_headers)
66
S3method(close,httr2_response)
7+
S3method(format,httr2_redacted)
78
S3method(print,httr2_cmd)
89
S3method(print,httr2_headers)
910
S3method(print,httr2_oauth_client)
@@ -12,7 +13,9 @@ S3method(print,httr2_request)
1213
S3method(print,httr2_response)
1314
S3method(print,httr2_token)
1415
S3method(print,httr2_url)
16+
S3method(str,httr2_headers)
1517
S3method(str,httr2_obfuscated)
18+
S3method(str,httr2_redacted)
1619
export("%>%")
1720
export(curl_help)
1821
export(curl_translate)

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# httr2 (development version)
22

3+
* `str()` correctly redacts redacted headers (#682).
4+
* `req_headers()` replaces existing headers with different case (#682).
35
* New `local_verbosity()` (#687).
46
* Can now use `HTTR2_VERBOSITY` env var to control default verbosity (#687).
57
* `req_perform_parallel(pool)` has been deprecated in favour of a new `max_active` argument (#681).

R/headers.R

+38-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
as_headers <- function(x, error_call = caller_env()) {
1+
as_headers <- function(x, redact = character(), error_call = caller_env()) {
22
if (is.character(x) || is.raw(x)) {
33
parsed <- curl::parse_headers(x)
44
valid <- parsed[grepl(":", parsed, fixed = TRUE)]
55
halves <- parse_in_half(valid, ":")
66

77
headers <- set_names(trimws(halves$right), halves$left)
8-
new_headers(as.list(headers), error_call = error_call)
8+
new_headers(as.list(headers), redact = redact, error_call = error_call)
99
} else if (is.list(x)) {
10-
new_headers(x, error_call = error_call)
10+
new_headers(x, redact = redact, error_call = error_call)
1111
} else {
1212
cli::cli_abort(
1313
"{.arg headers} must be a list, character vector, or raw.",
@@ -16,39 +16,54 @@ as_headers <- function(x, error_call = caller_env()) {
1616
}
1717
}
1818

19-
new_headers <- function(x, error_call = caller_env()) {
19+
new_headers <- function(x, redact = character(), error_call = caller_env()) {
2020
if (!is_list(x)) {
2121
cli::cli_abort("{.arg x} must be a list.", call = error_call)
2222
}
2323
if (length(x) > 0 && !is_named(x)) {
2424
cli::cli_abort("All elements of {.arg x} must be named.", call = error_call)
2525
}
2626

27-
structure(x, class = "httr2_headers")
27+
structure(x, redact = redact, class = "httr2_headers")
2828
}
2929

3030
#' @export
3131
print.httr2_headers <- function(x, ..., redact = TRUE) {
32-
cli::cli_text("{.cls {class(x)}}")
32+
cli::cat_line(cli::format_inline("{.cls {class(x)}}"))
33+
show_headers(x, redact = redact)
34+
invisible(x)
35+
}
36+
37+
show_headers <- function(x, redact = TRUE) {
3338
if (length(x) > 0) {
34-
cli::cat_line(cli::style_bold(names(x)), ": ", headers_redact(x, redact))
39+
vals <- lapply(headers_redact(x, redact), format)
40+
cli::cat_line(cli::style_bold(names(x)), ": ", vals)
3541
}
36-
invisible(x)
3742
}
3843

39-
headers_redact <- function(x, redact = TRUE, to_redact = NULL) {
44+
#' @export
45+
str.httr2_headers <- function(object, ..., no.list = FALSE) {
46+
object <- unclass(headers_redact(object))
47+
cat(" <httr2_headers>\n")
48+
utils::str(object, ..., no.list = TRUE)
49+
}
50+
51+
headers_redact <- function(x, redact = TRUE) {
4052
if (!redact) {
4153
x
4254
} else {
43-
to_redact <- union(attr(x, "redact"), to_redact)
55+
to_redact <- attr(x, "redact")
4456
attr(x, "redact") <- NULL
4557

4658
list_redact(x, to_redact, case_sensitive = FALSE)
4759
}
4860
}
4961

62+
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
5063
headers_flatten <- function(x) {
51-
set_names(as.character(unlist(x, use.names = FALSE)), rep(names(x), lengths(x)))
64+
n <- lengths(x)
65+
x[n > 1] <- lapply(x[n > 1], paste, collapse = ",")
66+
x
5267
}
5368

5469
list_redact <- function(x, names, case_sensitive = TRUE) {
@@ -57,15 +72,25 @@ list_redact <- function(x, names, case_sensitive = TRUE) {
5772
} else {
5873
i <- match(tolower(names), tolower(names(x)))
5974
}
60-
x[i] <- redacted()
75+
x[i] <- list(redacted())
6176
x
6277
}
6378

6479
redacted <- function() {
80+
structure(list(NULL), class = "httr2_redacted")
81+
}
82+
83+
#' @export
84+
format.httr2_redacted <- function(x, ...) {
6585
cli::col_grey("<REDACTED>")
6686
}
87+
#' @export
88+
str.httr2_redacted <- function(object, ...) {
89+
cat(" ", cli::col_grey("<REDACTED>"), "\n", sep = "")
90+
}
91+
6792
is_redacted <- function(x) {
68-
is.character(x) && length(x) == 1 && x == redacted()
93+
inherits(x, "httr2_redacted")
6994
}
7095

7196

R/oauth-token.R

+4-5
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,12 @@ oauth_token <- function(
5555
#' @export
5656
print.httr2_token <- function(x, ...) {
5757
cli::cli_text(cli::style_bold("<", paste(class(x), collapse = "/"), ">"))
58-
redacted <- list_redact(x, c("access_token", "refresh_token", "id_token"))
59-
if (has_name(redacted, "expires_at")) {
60-
redacted$expires_at <- format(.POSIXct(x$expires_at))
58+
if (has_name(x, "expires_at")) {
59+
x$expires_at <- format(.POSIXct(x$expires_at))
6160
}
6261

63-
bullets(compact(redacted))
64-
62+
redacted <- list_redact(compact(x), c("access_token", "refresh_token", "id_token"))
63+
bullets(redacted)
6564
invisible(x)
6665
}
6766

R/req-cache.R

+3-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ cache_body <- function(cached_resp, path = NULL) {
276276
# https://www.rfc-editor.org/rfc/rfc7232#section-4.1
277277
cache_headers <- function(cached_resp, resp) {
278278
check_response(cached_resp)
279-
as_headers(modify_list(cached_resp$headers, !!!resp$headers))
279+
280+
headers <- modify_list(cached_resp$headers, !!!resp$headers, .ignore_case = TRUE)
281+
as_headers(headers)
280282
}
281283

282284
# Caching headers ---------------------------------------------------------

R/req-dry-run.R

+2-6
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,14 @@ req_dry_run <- function(req,
6666
if (!quiet) {
6767
cli::cat_line(resp$method, " ", resp$path, " HTTP/1.1")
6868

69-
headers <- headers_redact(
70-
as_headers(as.list(resp$headers)),
71-
redact = redact_headers,
72-
to_redact = attr(req$headers, "redact")
73-
)
69+
headers <- new_headers(as.list(resp$headers), attr(req$headers, "redact"))
7470
if (testing_headers) {
7571
# curl::curl_echo() overrides
7672
headers$host <- NULL
7773
headers$`content-length` <- NULL
7874
}
7975

80-
cli::cat_line(cli::style_bold(names(headers)), ": ", headers)
76+
show_headers(headers)
8177
cli::cat_line()
8278
show_body(resp$body, headers$`content-type`, pretty_json = pretty_json)
8379
}

R/req-headers.R

+6-6
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ req_headers <- function(.req, ..., .redact = NULL) {
6565
check_request(.req)
6666
check_character(.redact, allow_null = TRUE)
6767

68-
redact_prev <- attr(.req$headers, "redact")
69-
header_names <- names(list2(...))
70-
.req$headers <- modify_list(.req$headers, ...)
68+
headers <- modify_list(.req$headers, ..., .ignore_case = TRUE)
7169

72-
.redact <- union(.redact, "Authorization")
73-
.redact <- .redact[tolower(.redact) %in% tolower(header_names)]
74-
attr(.req$headers, "redact") <- sort(union(.redact, redact_prev))
70+
redact <- union(.redact, "Authorization")
71+
redact <- redact[tolower(redact) %in% tolower(names(headers))]
72+
redact <- sort(union(redact, attr(.req$headers, "redact")))
73+
74+
.req$headers <- new_headers(headers, redact)
7575

7676
.req
7777
}

R/req-verbose.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ verbose_header <- function(prefix, x, redact = TRUE, to_redact = NULL) {
100100

101101
for (line in lines) {
102102
if (grepl("^[-a-zA-z0-9]+:", line)) {
103-
header <- headers_redact(as_headers(line), redact, to_redact = to_redact)
103+
header <- headers_redact(as_headers(line, to_redact), redact)
104104
cli::cat_line(prefix, cli::style_bold(names(header)), ": ", header)
105105
} else {
106106
cli::cat_line(prefix, line)

R/resp-headers.R

+2-1
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,13 @@ resp_retry_after <- function(resp) {
169169
#' resp_link_url(resp, "last")
170170
#' resp_link_url(resp, "prev")
171171
resp_link_url <- function(resp, rel) {
172-
if (!resp_header_exists(resp, "Link")) {
172+
if (!resp_header_exists(resp, "Link")) {
173173
return()
174174
}
175175

176176
headers <- resp_headers(resp)
177177
link_headers <- headers[tolower(names(headers)) == "link"]
178+
178179
links <- unlist(lapply(link_headers, parse_link), recursive = FALSE)
179180
sel <- map_lgl(links, ~ .$rel == rel)
180181
if (sum(sel) != 1L) {

R/utils.R

+12-7
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ bullets_with_header <- function(header, x) {
1010
bullets <- function(x) {
1111
as_simple <- function(x) {
1212
if (is.atomic(x) && length(x) == 1) {
13-
if (is_redacted(x)) {
14-
x
15-
} else if (is.character(x)) {
13+
if (is.character(x)) {
1614
paste0('"', x, '"')
1715
} else {
1816
format(x)
1917
}
2018
} else {
21-
paste0("<", class(x)[[1L]], ">")
19+
if (is_redacted(x)) {
20+
format(x)
21+
} else {
22+
paste0("<", class(x)[[1L]], ">")
23+
}
2224
}
2325
}
2426
vals <- map_chr(x, as_simple)
@@ -30,7 +32,7 @@ bullets <- function(x) {
3032
}
3133
}
3234

33-
modify_list <- function(.x, ..., error_call = caller_env()) {
35+
modify_list <- function(.x, ..., .ignore_case = FALSE, error_call = caller_env()) {
3436
dots <- list2(...)
3537
if (length(dots) == 0) return(.x)
3638

@@ -41,9 +43,12 @@ modify_list <- function(.x, ..., error_call = caller_env()) {
4143
)
4244
}
4345

46+
if (.ignore_case) {
47+
out <- .x[!tolower(names(.x)) %in% tolower(names(dots))]
48+
} else {
49+
out <- .x[!names(.x) %in% names(dots)]
50+
}
4451

45-
46-
out <- .x[!names(.x) %in% names(dots)]
4752
out <- c(out, compact(dots))
4853

4954
if (length(out) == 0) {

tests/testthat/_snaps/curl.md

+2-4
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,14 @@
1818

1919
Code
2020
print(curl_simplify_headers(headers, simplify_headers = TRUE))
21-
Message
22-
<httr2_headers>
2321
Output
22+
<httr2_headers>
2423
Accept: application/vnd.api+json
2524
user-agent: agent
2625
Code
2726
print(curl_simplify_headers(headers, simplify_headers = FALSE))
28-
Message
29-
<httr2_headers>
3027
Output
28+
<httr2_headers>
3129
Sec-Fetch-Dest: empty
3230
Sec-Fetch-Mode: cors
3331
sec-ch-ua-mobile: ?0

tests/testthat/_snaps/headers.md

+17-3
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,30 @@
1010

1111
Code
1212
as_headers(c("X:1", "Y: 2", "Z:"))
13-
Message
14-
<httr2_headers>
1513
Output
14+
<httr2_headers>
1615
X: 1
1716
Y: 2
1817
Z:
1918
Code
2019
as_headers(list())
21-
Message
20+
Output
21+
<httr2_headers>
22+
23+
# print and str redact headers
24+
25+
Code
26+
print(x)
27+
Output
2228
<httr2_headers>
29+
x: <REDACTED>
30+
y: 2
31+
Code
32+
str(x)
33+
Output
34+
<httr2_headers>
35+
$ x: <REDACTED>
36+
$ y: num 2
2337

2438
# new_headers checks inputs
2539

tests/testthat/_snaps/oauth-token.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# new token computes expires_at
2+
3+
Code
4+
token
5+
Message
6+
<httr2_token>
7+
* token_type : "bearer"
8+
* access_token: <REDACTED>
9+
* expires_at : "2025-02-19 21:20:10"
10+
111
# printing token redacts access, id and refresh token
212

313
Code

tests/testthat/_snaps/req.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@
4040
<httr2_request>
4141
GET https://example.com
4242
Headers:
43-
* A: "1"
44-
* A: "2"
45-
* A: "3"
43+
* A: "1,2,3"
4644
Body: empty
4745

4846
# check_request() gives useful error

tests/testthat/test-headers.R

+12-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ test_that("has nice print method", {
1818
})
1919
})
2020

21+
test_that("print and str redact headers", {
22+
x <- new_headers(list(x = 1, y = 2), redact = "x")
23+
expect_snapshot({
24+
print(x)
25+
str(x)
26+
})
27+
})
28+
2129
test_that("subsetting is case insensitive", {
2230
x <- new_headers(list(x = 1))
2331
expect_equal(x$X, 1)
@@ -30,7 +38,7 @@ test_that("redaction is case-insensitive", {
3038
attr(headers, "redact") <- "Authorization"
3139
redacted <- headers_redact(headers)
3240
expect_named(redacted, "AUTHORIZATION")
33-
expect_match(as.character(redacted$AUTHORIZATION), "<REDACTED>")
41+
expect_true(is_redacted(redacted$AUTHORIZATION))
3442
})
3543

3644
test_that("new_headers checks inputs", {
@@ -41,7 +49,7 @@ test_that("new_headers checks inputs", {
4149
})
4250

4351
test_that("can flatten repeated inputs", {
44-
expect_equal(headers_flatten(list()), character())
45-
expect_equal(headers_flatten(list(x = 1)), c(x = "1"))
46-
expect_equal(headers_flatten(list(x = 1:2)), c(x = "1", x = "2"))
52+
expect_equal(headers_flatten(list()), list())
53+
expect_equal(headers_flatten(list(x = 1)), list(x = 1))
54+
expect_equal(headers_flatten(list(x = 1:2)), list(x = "1,2"))
4755
})

tests/testthat/test-oauth-client.R

+4-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ test_that("can authenticate using header or body", {
4949

5050
req <- request("http://example.com")
5151
req_h <- oauth_client_req_auth(req, client("header"))
52-
expect_equal(req_h$headers, structure(list(Authorization = "Basic aWQ6c2VjcmV0"), redact = "Authorization"))
52+
expect_equal(
53+
req_h$headers,
54+
new_headers(list(Authorization = "Basic aWQ6c2VjcmV0"), "Authorization")
55+
)
5356

5457
req_b <- oauth_client_req_auth(req, client("body"))
5558
expect_equal(req_b$body$data, list(client_id = I("id"), client_secret = I("secret")))

0 commit comments

Comments
 (0)