-
Notifications
You must be signed in to change notification settings - Fork 80
feat: add httr2_translate #797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
f22105b
86e8255
be8141d
3b030f3
3e5c4e4
822fa8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| #' Translate httr2 request to curl command | ||
| #' | ||
| #' Convert an httr2 request object to equivalent curl command line syntax. | ||
| #' This is useful for debugging, sharing requests, or converting to other tools. | ||
| #' | ||
| #' @param .req An httr2 request object created with [request()]. | ||
| #' @return A character string containing the curl command. | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #' @export | ||
| #' @examples | ||
| #' \dontrun{ | ||
| #' # Basic GET request | ||
| #' request("https://httpbin.org/get") |> | ||
| #' httr2_translate() | ||
| #' | ||
| #' # POST with JSON body | ||
| #' request("https://httpbin.org/post") |> | ||
| #' req_body_json(list(name = "value")) |> | ||
| #' httr2_translate() | ||
| #' | ||
| #' # POST with form data | ||
| #' request("https://httpbin.org/post") |> | ||
| #' req_body_form(name = "value") |> | ||
| #' httr2_translate() | ||
| #' } | ||
| httr2_translate <- function(.req) { | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # validate the request | ||
| check_request(.req) | ||
|
|
||
| # Extract URL | ||
| url <- .req$url | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # use the request's method if it is set, otherwise infer | ||
| method <- .req$method %||% | ||
| { | ||
| if (!is.null(.req$body$data)) { | ||
| "POST" | ||
| } else { | ||
| "GET" | ||
| } | ||
| } | ||
|
|
||
| # we will append to cmd_parts to build up the request | ||
| cmd_parts <- c("curl") | ||
|
|
||
| # if the method isn't GET, it needs to be specified with `-X` | ||
| if (method != "GET") { | ||
| cmd_parts <- c(cmd_parts, paste0("-X ", method)) | ||
| } | ||
|
|
||
| # if headers are present, add them using -H flag | ||
| if (!is.null(.req$headers) && length(.req$headers) > 0) { | ||
| headers <- .req$headers | ||
| for (name in names(headers)) { | ||
| value <- headers[[name]] | ||
|
|
||
| # handle weakrefs | ||
| if (rlang::is_weakref(value)) { | ||
| value <- rlang::wref_value(value) | ||
| } | ||
|
|
||
| # unobfuscate obfuscated | ||
| if (is_obfuscated(value)) { | ||
| value <- unobfuscate(value, handle = "reveal") | ||
| } | ||
|
|
||
| cmd_parts <- c(cmd_parts, paste0('-H "', name, ': ', value, '"')) | ||
| } | ||
| } | ||
|
|
||
| # manage options | ||
| if (!is.null(.req$options) && length(.req$options) > 0) { | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| options <- .req$options | ||
| for (name in names(options)) { | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| value <- options[[name]] | ||
| # convert options to curl flags | ||
| curl_flag <- switch( | ||
| name, | ||
| "timeout" = paste0("--max-time ", value), | ||
| "connecttimeout" = paste0("--connect-timeout ", value), | ||
| "proxy" = paste0("--proxy ", value), | ||
| "useragent" = paste0('--user-agent "', value, '"'), | ||
| "referer" = paste0('--referer "', value, '"'), | ||
| "followlocation" = if (value) "--location" else NULL, | ||
| "ssl_verifypeer" = if (!value) "--insecure" else NULL, | ||
| "verbose" = if (value) "--verbose" else NULL, | ||
| "cookiejar" = paste0('--cookie-jar "', value, '"'), | ||
| "cookiefile" = paste0('--cookie "', value, '"'), | ||
| # for unknown options try guess the flag if it was the intention | ||
| paste0("--", gsub("_", "-", name), " ", value) | ||
| ) | ||
| if (!is.null(curl_flag)) { | ||
| cmd_parts <- c(cmd_parts, curl_flag) | ||
| } | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (!is.null(.req$body)) { | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| body_type <- .req$body$type %||% "empty" | ||
| # if content_type set here we use it | ||
| content_type <- .req$body$content_type | ||
|
|
||
| # if content_type not set we need to infer from body type | ||
| if (is.null(content_type) || !nzchar(content_type)) { | ||
| if (body_type == "json") { | ||
| content_type <- "application/json" | ||
| } else if (body_type == "form") { | ||
| content_type <- "application/x-www-form-urlencoded" | ||
| } | ||
| } | ||
|
|
||
| # add content-type header if we have one and it's not already set | ||
| if (!is.null(content_type)) { | ||
| if ( | ||
| is.null(.req$headers) || | ||
| !("content-type" %in% tolower(names(.req$headers))) | ||
| ) { | ||
| cmd_parts <- c( | ||
| cmd_parts, | ||
| paste0('-H "Content-Type: ', content_type, '"') | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| # add body data | ||
| switch( | ||
| body_type, | ||
| "string" = { | ||
| data <- .req$body$data | ||
| cmd_parts <- c(cmd_parts, paste0('-d "', gsub('"', '\\"', data), '"')) | ||
| }, | ||
| "raw" = { | ||
| # Raw bytes - use --data-binary | ||
| cmd_parts <- c(cmd_parts, '--data-binary "@-"') | ||
| }, | ||
| "file" = { | ||
| path <- .req$body$data | ||
| cmd_parts <- c(cmd_parts, paste0('--data-binary "@', path, '"')) | ||
| }, | ||
| "json" = { | ||
| data <- unobfuscate(.req$body$data, handle = "reveal") | ||
| json_data <- jsonlite::toJSON(data, auto_unbox = TRUE) | ||
| cmd_parts <- c(cmd_parts, paste0('-d \'', json_data, '\'')) | ||
| }, | ||
| "form" = { | ||
| form_data <- unobfuscate(.req$body$data, handle = "reveal") | ||
| form_string <- paste( | ||
| names(form_data), | ||
| form_data, | ||
| sep = "=", | ||
| collapse = "&" | ||
| ) | ||
| cmd_parts <- c(cmd_parts, paste0('-d "', form_string, '"')) | ||
| }, | ||
| "multipart" = { | ||
| form_data <- unobfuscate(.req$body$data, handle = "reveal") | ||
| for (name in names(form_data)) { | ||
| value <- form_data[[name]] | ||
| cmd_parts <- c(cmd_parts, paste0('-F "', name, '=', value, '"')) | ||
| } | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| cmd_parts <- c(cmd_parts, paste0('"', url, '"')) | ||
|
|
||
| # join all parts with proper formatting | ||
JosiahParry marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (length(cmd_parts) <= 2) { | ||
| paste(cmd_parts, collapse = " ") | ||
| } else { | ||
|
||
| # need to ensure that "curl" isn't on its own line | ||
| # for compatibility with curl_translate() | ||
| first_part <- paste(cmd_parts[1:2], collapse = " ") | ||
| remaining_parts <- cmd_parts[-(1:2)] | ||
|
|
||
| if (length(remaining_parts) == 0) { | ||
| first_part | ||
| } else { | ||
| formatted_parts <- paste0(" ", remaining_parts, " \\") | ||
| # Remove the trailing backslash from the last part | ||
| formatted_parts[length(formatted_parts)] <- gsub( | ||
| " \\\\$", | ||
| "", | ||
| formatted_parts[length(formatted_parts)] | ||
| ) | ||
|
|
||
| paste(c(paste0(first_part, " \\"), formatted_parts), collapse = "\n") | ||
| } | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # httr2_translate() works with basic GET requests | ||
|
|
||
| Code | ||
| httr2_translate(request("https://httpbin.org/get")) | ||
| Output | ||
| [1] "curl \"https://httpbin.org/get\"" | ||
|
|
||
| # httr2_translate() works with POST methods | ||
|
|
||
| Code | ||
| httr2_translate(req_method(request("https://httpbin.org/post"), "POST")) | ||
| Output | ||
| [1] "curl -X POST \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with headers | ||
|
|
||
| Code | ||
| httr2_translate(req_headers(request("https://httpbin.org/get"), Accept = "application/json", | ||
| `User-Agent` = "httr2/1.0")) | ||
| Output | ||
| [1] "curl -H \"Accept: application/json\" \\\n -H \"User-Agent: httr2/1.0\" \\\n \"https://httpbin.org/get\"" | ||
|
|
||
| # httr2_translate() works with JSON bodies | ||
|
|
||
| Code | ||
| httr2_translate(req_body_json(request("https://httpbin.org/post"), list(name = "test", | ||
| value = 123))) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"test\",\"value\":123}' \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with form bodies | ||
|
|
||
| Code | ||
| httr2_translate(req_body_form(request("https://httpbin.org/post"), name = "test", | ||
| value = "123")) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"name=test&value=123\" \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with multipart bodies | ||
|
|
||
| Code | ||
| httr2_translate(req_body_multipart(request("https://httpbin.org/post"), name = "test", | ||
| value = "123")) | ||
| Output | ||
| [1] "curl -X POST \\\n -F \"name=test\" \\\n -F \"value=123\" \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with string bodies | ||
|
|
||
| Code | ||
| httr2_translate(req_body_raw(request("https://httpbin.org/post"), "test data", | ||
| type = "text/plain")) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n -d \"test data\" \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with file bodies | ||
|
|
||
| Code | ||
| httr2_translate(req_body_file(request("https://httpbin.org/post"), path, type = "text/plain")) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: text/plain\" \\\n --data-binary \"@<tempfile>\" \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with custom content types | ||
|
|
||
| Code | ||
| httr2_translate(req_body_json(request("https://httpbin.org/post"), list(test = "data"), | ||
| type = "application/vnd.api+json")) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: application/vnd.api+json\" \\\n -d '{\"test\":\"data\"}' \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with options | ||
|
|
||
| Code | ||
| httr2_translate(req_options(request("https://httpbin.org/get"), timeout = 30, | ||
| verbose = TRUE, ssl_verifypeer = FALSE)) | ||
| Output | ||
| [1] "curl --max-time 30 \\\n --verbose \\\n --insecure \\\n \"https://httpbin.org/get\"" | ||
|
|
||
| # httr2_translate() works with cookies | ||
|
|
||
| Code | ||
| httr2_translate(req_options(request("https://httpbin.org/cookies"), cookiejar = cookie_file, | ||
| cookiefile = cookie_file)) | ||
| Output | ||
| [1] "curl --cookie-jar \"<cookie-file>\" \\\n --cookie \"<cookie-file>\" \\\n \"https://httpbin.org/cookies\"" | ||
|
|
||
| # httr2_translate() works with obfuscated values in headers | ||
|
|
||
| Code | ||
| httr2_translate(req_headers(request("https://httpbin.org/get"), Authorization = obfuscated( | ||
| "ZdYJeG8zwISodg0nu4UxBhs"))) | ||
| Output | ||
| [1] "curl -H \"Authorization: y\" \\\n \"https://httpbin.org/get\"" | ||
|
|
||
| # httr2_translate() works with obfuscated values in JSON body | ||
|
|
||
| Code | ||
| httr2_translate(req_body_json(request("https://httpbin.org/post"), list( | ||
| username = "test", password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs")))) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d '{\"username\":\"test\",\"password\":\"y\"}' \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with obfuscated values in form body | ||
|
|
||
| Code | ||
| httr2_translate(req_body_form(request("https://httpbin.org/post"), username = "test", | ||
| password = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"))) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"username=test&password=y\" \\\n \"https://httpbin.org/post\"" | ||
|
|
||
| # httr2_translate() works with complex requests | ||
|
|
||
| Code | ||
| httr2_translate(req_options(req_body_json(req_headers(req_method(request( | ||
| "https://api.github.com/user/repos"), "POST"), Accept = "application/vnd.github.v3+json", | ||
| Authorization = obfuscated("ZdYJeG8zwISodg0nu4UxBhs"), `User-Agent` = "MyApp/1.0"), | ||
| list(name = "test-repo", description = "A test repository", private = TRUE)), | ||
| timeout = 60)) | ||
| Output | ||
| [1] "curl -X POST \\\n -H \"Accept: application/vnd.github.v3+json\" \\\n -H \"Authorization: y\" \\\n -H \"User-Agent: MyApp/1.0\" \\\n --max-time 60 \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"test-repo\",\"description\":\"A test repository\",\"private\":true}' \\\n \"https://api.github.com/user/repos\"" | ||
|
|
||
| # httr2_translate() works with simple requests (single line) | ||
|
|
||
| Code | ||
| httr2_translate(request("https://httpbin.org/get")) | ||
| Output | ||
| [1] "curl \"https://httpbin.org/get\"" | ||
|
|
||
| # httr2_translate() validates input | ||
|
|
||
| Code | ||
| httr2_translate("not a request") | ||
| Condition | ||
| Error in `httr2_translate()`: | ||
| ! `.req` must be an HTTP request object, not the string "not a request". | ||
|
|
Uh oh!
There was an error while loading. Please reload this page.