diff --git a/DESCRIPTION b/DESCRIPTION index e1767ac7..4e16153f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,9 +13,8 @@ Depends: Imports: digest, jsonlite, - methods, mime, - RCurl (>= 1.95-0), + curl (>= 0.5.9001), R6, stringr (>= 0.6.1) Suggests: diff --git a/NAMESPACE b/NAMESPACE index d1e8e6df..4610d429 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,33 +3,22 @@ S3method("$",insensitive) S3method("[",insensitive) S3method("[[",insensitive) +S3method(as.character,form_file) S3method(as.character,response) -S3method(c,config) +S3method(c,request) S3method(cookies,handle) S3method(cookies,response) S3method(headers,response) S3method(length,path) S3method(print,cache_info) -S3method(print,config) S3method(print,handle) S3method(print,oauth_app) S3method(print,oauth_endpoint) S3method(print,opts_list) +S3method(print,request) S3method(print,response) -S3method(print,write_disk) -S3method(print,write_memory) -S3method(print,write_stream) S3method(status_code,numeric) S3method(status_code,response) -S3method(write_init,write_disk) -S3method(write_init,write_memory) -S3method(write_init,write_stream) -S3method(write_opts,write_disk) -S3method(write_opts,write_memory) -S3method(write_opts,write_stream) -S3method(write_term,write_disk) -S3method(write_term,write_memory) -S3method(write_term,write_stream) export(BROWSE) export(DELETE) export(GET) @@ -47,7 +36,6 @@ export(accept_json) export(accept_xml) export(add_headers) export(authenticate) -export(brew_dr) export(build_url) export(cache_info) export(config) @@ -67,6 +55,7 @@ export(hmac_sha1) export(http_condition) export(http_date) export(http_status) +export(httr_dr) export(httr_options) export(init_oauth1.0) export(init_oauth2.0) @@ -111,13 +100,7 @@ export(with_config) export(with_verbose) export(write_disk) export(write_function) -export(write_init) export(write_memory) -export(write_opts) export(write_stream) -export(write_term) import(stringr) importFrom(R6,R6Class) -useDynLib(httr,close_file) -useDynLib(httr,write_callback) -useDynLib(httr,writer) diff --git a/NEWS.md b/NEWS.md index 228acf40..c9f3e1d9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,24 @@ # httr 0.6.1.9000 -* `config()` and `c.config()` now clean up duplicated options (#213). +* The biggest change in this version is that httr no longer uses the RCurl + package. Instead it uses the curl package, fresh binding to libcurl + written by Jeroen Ooms (#172). This should make httr more reliable and + no longer prone to the "easy handle already used in multi handle" error. + This change shouldn't affect any code that uses httr - all the changes + have happened behind the scenes. + +* `cookies` argument to `handle()` is deprecated - cookies are always + turned on by default. + +* `brew_dr()` has been renamed to `httr_dr()` - that's what it should've + been in the first place! + +* Uses `CURL_CA_BUNDLE` environment variable to look for cert bundle on + Windows (#223). + +* `safe_callback()` is deprecated - it's no longer needed with curl. + +* `config()` now clean up duplicated options (#213). * `POST()` and `PUT()` now clean up after themselves when uploading a single file (@mtmorgan). diff --git a/R/body.R b/R/body.R index 476be987..c8f39029 100644 --- a/R/body.R +++ b/R/body.R @@ -1,9 +1,11 @@ body_config <- function(body = NULL, encode = "form", type = NULL) { # Post without body - if (is.null(body)) return(body_raw(raw())) + if (is.null(body)) + return(body_raw(raw())) # No body - if (identical(body, FALSE)) return(body_httr(post = TRUE, nobody = TRUE)) + if (identical(body, FALSE)) + return(config(post = TRUE, nobody = TRUE)) # For character/raw, send raw bytes if (is.character(body) || is.raw(body)) { @@ -11,22 +13,22 @@ body_config <- function(body = NULL, encode = "form", type = NULL) { } # Send single file lazily - if (inherits(body, "FileUploadInfo")) { - con <- file(body$filename, "rb") - mime_type <- body$contentType %||% - mime::guess_type(body$filename, empty = NULL) - size <- file.info(body$filename)$size + if (inherits(body, "form_file")) { + con <- file(body$path, "rb") + size <- file.info(body$path)$size - return(body_httr( - post = TRUE, - readfunction = function(nbytes, ...) { - bin <- readBin(con, "raw", nbytes) - if (!length(bin)) - close(con) - bin - }, - postfieldsize = size, - type = mime_type + return(c( + config( + post = TRUE, + readfunction = function(nbytes, ...) { + bin <- readBin(con, "raw", nbytes) + if (!length(bin)) + close(con) + bin + }, + postfieldsize = size + ), + content_type(body$type) )) } @@ -41,50 +43,34 @@ body_config <- function(body = NULL, encode = "form", type = NULL) { } else if (encode == "json") { body_raw(jsonlite::toJSON(body, auto_unbox = TRUE), "application/json") } else if (encode == "multipart") { - # For multipart, rely on RCurl .postForm function to make it possible - # to intermingle on-disk and in-memory content. - - charify <- function(x) { - if (inherits(x, "FileUploadInfo")) return(x) - as.character(x) - } - body <- lapply(body, charify) + body <- lapply(body, as.character) stopifnot(length(names(body)) > 0) - body_rcurl(body = body, style = NA) + request(fields = body) } else { stop("Unknown encoding", call. = FALSE) } } - -body_rcurl <- function(body = NULL, style = NULL) { - list( - config = NULL, - body = body, - style = style, - curl_post = TRUE - ) -} - -body_httr <- function(..., type = NULL) { - list( - config = c(config(...), content_type(type)), - curl_post = FALSE - ) -} - body_raw <- function(body, type = NULL) { if (!is.raw(body)) { body <- charToRaw(paste(body, collapse = "\n")) } - base <- body_httr( - post = TRUE, - postfieldsize = length(body), - postfields = body, - type = type %||% "" # For raw bodies, override default POST content-type + c( + config( + post = TRUE, + postfieldsize = length(body), + postfields = body + ), + # For raw bodies, override default POST content-type + content_type(type %||% "") ) +} - base +body_httr <- function(..., type = NULL) { + request() + list( + config = c(config(...), content_type(type)) + ) } diff --git a/R/cache.R b/R/cache.R index 44d1041c..6294d5ec 100644 --- a/R/cache.R +++ b/R/cache.R @@ -114,7 +114,7 @@ parse_cache_control <- function(x) { rerequest <- function(r) { x <- cache_info(r) if (!x$cacheable) { - return(reperform(r$request)) + return(reperform(r)) } # Cacheable, and hasn't expired @@ -123,14 +123,13 @@ rerequest <- function(r) { } # Requires validation - r$request$opts <- c( - r$request$opts, - add_headers( + req <- c(r$request, + request(headers = c( `If-Modified-Since` = http_date(x$modified), `If-None-Match` = x$etag - ) + )) ) - validated <- reperform(r$request) + validated <- request_perform(req, r$handle) if (status_code(validated) == 304L) { r @@ -139,6 +138,6 @@ rerequest <- function(r) { } } -reperform <- function(x) { - perform(x$handle, x$writer, x$method, x$opts, x$body) +reperform <- function(resp) { + request_perform(resp$request, resp$handle) } diff --git a/R/config.r b/R/config.r index 81335515..146a409e 100644 --- a/R/config.r +++ b/R/config.r @@ -13,6 +13,7 @@ #' #' @seealso \code{\link{set_config}} to set global config defaults, and #' \code{\link{with_config}} to temporarily run code with set options. +#' @param token An OAuth token (1.0 or 2.0) #' @family config #' @family ways to set configuration #' @seealso All known available options are listed in \code{\link{httr_options}} @@ -40,32 +41,10 @@ #' # options. But you can pass Curl options (as listed in httr_options()) #' # in config #' HEAD("https://www.google.com/", config(verbose = TRUE)) -config <- function(...) { - options <- list(...) - - known <- c(RCurl::listCurlOptions(), "token", "writer") - unknown <- setdiff(names(options), known) - if (length(unknown) > 0) { - stop("Unknown RCurl options: ", paste0(unknown, collapse = ", ")) - } - - # Clean up duplicated options - headers <- names(options) == "httpheader" - if (any(headers)) { - all_headers <- unlist(unname(options[headers])) - all_headers <- all_headers[!duplicated(names(all_headers), fromLast = TRUE)] - - options <- options[!headers] - options[["httpheader"]] <- all_headers - } - options <- options[!duplicated(names(options), fromLast = TRUE)] - - class(options) <- "config" - options +config <- function(..., token = NULL) { + request(options = list(...), auth_token = token) } -is.config <- function(x) inherits(x, "config") - #' List available options. #' #' This function lists all available options for \code{\link{config}()}. @@ -96,16 +75,15 @@ is.config <- function(x) inherits(x, "config") #' curl_docs("CURLOPT_USERPWD") httr_options <- function(matches) { - constants <- RCurl::getCurlOptionsConstants() + constants <- curl::curl_options() constants <- constants[order(names(constants))] - rcurl <- names(constants) - curl <- translate_curl(rcurl) + rcurl <- tolower(names(constants)) opts <- data.frame( httr = rcurl, libcurl = translate_curl(rcurl), - type = unname(RCurl::getCurlOptionTypes(constants)), + type = curl_option_types(constants), stringsAsFactors = FALSE ) @@ -118,6 +96,13 @@ httr_options <- function(matches) { opts } +curl_option_types <- function(opts = curl::curl_options()) { + type_name <- c("integer", "string", "function", "number") + type <- floor(opts / 10000) + + type_name[type + 1] +} + #' @export print.opts_list <- function(x, ...) { cat(paste0(format(names(x)), ": ", x, collapse = "\n"), "\n", sep = "") @@ -144,70 +129,10 @@ curl_docs <- function(x) { BROWSE(url) } -# Grepping http://curl.haxx.se/libcurl/c/curl_easy_setopt.html for -# "linked list", finds the follow options: -# -# CURLOPT_HTTPHEADER -# CURLOPT_HTTPPOST -# CURLOPT_HTTP200ALIASES -# CURLOPT_MAIL_RCPT -# CURLOPT_QUOTE -# CURLOPT_POSTQUOTE -# CURLOPT_PREQUOTE -# CURLOPT_RESOLVE -# -# Of these, only CURLOPT_HTTPHEADER is likely ever to be used, so we'll -# deal with it specially. It's possible you might also want to do that -# with cookies, but that would require a bigger rewrite. -#' @export -c.config <- function(...) { - Reduce(modify_config, list(...)) -} - -#' @export -print.config <- function(x, ...) { - cat("Config: \n") - str(unclass(x), give.head = FALSE) -} - -# A version of modifyList that works with config files, and merges -# http header -modify_config <- function(x, val) { - overwrite <- setdiff(names(val), "httpheader") - x[overwrite] <- val[overwrite] - - headers <- c(x$httpheader, val$httpheader) - x$httpheader <- add_headers(.headers = headers)$httpheader - - x -} - -make_config <- function(x, ...) { - configs <- c(list(x), unnamed(list(...))) - - structure(Reduce(modify_config, configs), class = "config") -} - -default_config <- function() { - cert <- system.file("cacert.pem", package = "httr") - - c(config( - followlocation = TRUE, - maxredirs = 10L, - encoding = "gzip" - ), - user_agent(default_ua()), - add_headers(Accept = "application/json, text/xml, application/xml, */*"), - write_memory(), - if (.Platform$OS.type == "windows") config(cainfo = cert), - getOption("httr_config") - ) -} - default_ua <- function() { versions <- c( - curl = RCurl::curlVersion()$version, - Rcurl = as.character(packageVersion("RCurl")), + libcurl = curl::curl_version()$version, + `r-curl` = as.character(packageVersion("curl")), httr = as.character(packageVersion("httr")) ) paste0(names(versions), "/", versions, collapse = " ") @@ -229,7 +154,7 @@ default_ua <- function() { #' reset_config() #' GET("http://google.com") set_config <- function(config, override = FALSE) { - stopifnot(is.config(config)) + stopifnot(is.request(config)) old <- getOption("httr_config") %||% config() if (!override) config <- c(old, config) @@ -256,7 +181,7 @@ reset_config <- function() set_config(config(), TRUE) #' # Or even easier: #' with_verbose(GET("http://google.com")) with_config <- function(config = config(), expr, override = FALSE) { - stopifnot(is.config(config)) + stopifnot(is.request(config)) old <- set_config(config, override) on.exit(set_config(old, override = TRUE)) diff --git a/R/cookies.r b/R/cookies.r index f52f3ca7..ac6f2ab6 100644 --- a/R/cookies.r +++ b/R/cookies.r @@ -15,7 +15,7 @@ set_cookies <- function(..., .cookies = character(0)) { cookies <- c(..., .cookies) stopifnot(is.character(cookies)) - cookies_str <- vapply(cookies, RCurl::curlEscape, FUN.VALUE = character(1)) + cookies_str <- vapply(cookies, curl::curl_escape, FUN.VALUE = character(1)) cookie <- paste(names(cookies), cookies_str, sep = "=", collapse = ";") @@ -37,13 +37,5 @@ cookies.response <- function(x) x$cookies #' @export cookies.handle <- function(x) { - raw <- RCurl::getCurlInfo(x$handle, "cookielist")[[1]] - if (length(raw) == 0) return(list()) - - parsed <- read.delim(text = raw, sep = "\t", header = FALSE, - stringsAsFactors = FALSE) - names(parsed) <- c("domain", "tailmatch", "path", "secure", "expires", "name", - "value") - - stats::setNames(as.list(parsed$value), parsed$name) + curl::handle_cookies(x$handle) } diff --git a/R/doctor.R b/R/doctor.R index fe59c580..4f186556 100644 --- a/R/doctor.R +++ b/R/doctor.R @@ -3,12 +3,12 @@ #' Currently one check: that curl uses nss. #' #' @export -brew_dr <- function() { +httr_dr <- function() { check_for_nss() } check_for_nss <- function() { - if (!grepl("^NSS", RCurl::curlVersion()$ssl_version)) return() + if (!grepl("^NSS", curl::curl_version()$ssl_version)) return() warning(' ------------------------------------------------------------------------ diff --git a/R/handle.r b/R/handle.r index a8a91b10..abefc014 100644 --- a/R/handle.r +++ b/R/handle.r @@ -5,8 +5,7 @@ #' it will mostly be hidden from the user. #' #' @param url full url to site -#' @param cookies if \code{TRUE} (the default), maintain cookies across -#' requests. +#' @param cookies DEPRECATED #' @export #' @examples #' handle("http://google.com") @@ -22,29 +21,20 @@ handle <- function(url, cookies = TRUE) { stopifnot(is.character(url), length(url) == 1) - url <- parse_url(url) - cookie_path <- if (cookies) tempfile() else NULL + if (!missing(cookies)) + warning("Cookies argument is depcrated", call. = FALSE) - h <- RCurl::getCurlHandle(cookiefile = cookie_path, .defaults = list()) + h <- curl::new_handle() structure(list(handle = h, url = url), class = "handle") } #' @export print.handle <- function(x, ...) { - cat("Host: ", build_url(x$url) , " <", ref(x), ">\n", sep = "") + cat("Host: ", x$url , " <", ref(x$handle), ">\n", sep = "") } ref <- function(x) { - str_extract(capture.output(print(x$handle@ref)), "0x[0-9a-f]*") + str_extract(capture.output(print(x))[1], "0x[0-9a-f]*") } is.handle <- function(x) inherits(x, "handle") - -reset_handle_config <- function(handle, config) { - # Calls curl_easy_reset (http://curl.haxx.se/libcurl/c/curl_easy_reset.html) - # Does not change live connections, session ID cache, DNS cache, cookies - # or shares. - RCurl::reset(handle$handle) - invisible(TRUE) -} - diff --git a/R/headers.r b/R/headers.r index a5e0c7f9..1142534e 100644 --- a/R/headers.r +++ b/R/headers.r @@ -39,14 +39,7 @@ headers.response <- function(x) { #' # Override default headers with empty strings #' GET("http://httpbin.org/headers", add_headers(Accept = "")) add_headers <- function(..., .headers = character()) { - headers <- c(..., .headers) - if (length(headers) == 0) return() - stopifnot(is.character(headers)) - - # Keep last of duplicated headers - headers <- headers[!duplicated(names(headers), fromLast = TRUE)] - - config(httpheader = headers) + request(headers = c(..., .headers)) } @@ -109,3 +102,48 @@ accept_json <- function() accept("application/json") #' @rdname content_type accept_xml <- function() accept("application/xml") + + + +# Parses a header lines as recieved from libcurl. Multiple responses +# will be intermingled, each separated by an http status line. +parse_headers <- function(raw) { + lines <- strsplit(rawToChar(raw), "\r?\n")[[1]] + + new_response <- grepl("^HTTP", lines) + grps <- cumsum(new_response) + + lapply(unname(split(lines, grps)), parse_single_header) +} + +parse_single_header <- function(lines) { + status <- parse_http_status(lines[[1]]) + + # Parse headers into name-value pairs + header_lines <- lines[lines != ""][-1] + pos <- regexec("^([^:]*):\\s*(.*)$", header_lines) + pieces <- regmatches(header_lines, pos) + + n <- vapply(pieces, length, integer(1)) + if (any(n != 3)) { + bad <- header_lines[n != 3] + pieces <- pieces[n == 3] + + warning("Failed to parse headers:\n", paste0(bad, "\n"), call. = FALSE) + } + + names <- vapply(pieces, "[[", 2, FUN.VALUE = character(1)) + values <- lapply(pieces, "[[", 3) + headers <- insensitive(stats::setNames(values, names)) + + list(status = status$status, version = status$version, headers = headers) +} + +parse_http_status <- function(x) { + status <- as.list(strsplit(x, "\\s+")[[1]]) + names(status) <- c("version", "status", "message")[seq_along(status)] + status$status <- as.integer(status$status) + + + status +} diff --git a/R/hmac.r b/R/hmac.r index 6de6d58e..8333ff80 100644 --- a/R/hmac.r +++ b/R/hmac.r @@ -8,6 +8,6 @@ #' @export hmac_sha1 <- function(key, string) { hash <- digest::hmac(key, string, "sha1", raw = TRUE) - RCurl::base64(hash) + base64enc::base64encode(hash) } diff --git a/R/http--request.r b/R/http--request.r deleted file mode 100644 index a52d36e6..00000000 --- a/R/http--request.r +++ /dev/null @@ -1,62 +0,0 @@ -make_request <- function(method, handle, url, config = NULL, body = NULL, - refresh = TRUE) { - method <- toupper(method) - if (is.null(config)) config <- config() - stopifnot(is.config(config)) - stopifnot(is.handle(handle)) - stopifnot(is.character(url), length(url) == 1) - - # Combine with default config - opts <- modify_config(default_config(), config) - if (method != "POST") { - opts$customrequest <- method - } - - # Sign request, if needed - token <- opts$token - if (!is.null(token)) { - signature <- token$sign(method, url) - - opts <- modify_config(opts, signature$config) - opts$token <- NULL - opts$url <- signature$url - } else { - opts$url <- url - } - - # Extract writer object - writer <- opts$writer - opts$writer <- NULL - - # Perform request and capture output - res <- perform(handle, writer, method, opts, body) - - needs_refresh <- refresh && res$status_code == 401L && - !is.null(config$token) && config$token$can_refresh() - if (needs_refresh) { - message("Auto-refreshing stale OAuth token.") - config$token$refresh() - - make_request(method, handle, url, config = config, body = body, - refresh = FALSE) - } else { - res - } -} - -last_request <- function(x) { - stopifnot(is.handle(x)) - RCurl::getCurlInfo(x[[1]]) -} - -request_times <- function(x) { - req <- last_request(x) - - c(redirect = req$redirect.time, - namelookup = req$namelookup.time, - connect = req$connect.time, - pretransfer = req$pretransfer.time, - starttransfer = req$starttransfer.time, - total = req$total.time) - -} diff --git a/R/http-delete.r b/R/http-delete.r index 35488e0b..28c9849a 100644 --- a/R/http-delete.r +++ b/R/http-delete.r @@ -28,7 +28,6 @@ #' POST("http://httpbin.org/delete") DELETE <- function(url = NULL, config = list(), ..., handle = NULL) { hu <- handle_url(handle, url, ...) - config <- make_config(config, ...) - - make_request("delete", hu$handle, hu$url, config) + req <- request_build("DELETE", hu$url, config, ...) + request_perform(req, hu$handle$handle) } diff --git a/R/http-get.r b/R/http-get.r index c6b17e55..9763f4d5 100644 --- a/R/http-get.r +++ b/R/http-get.r @@ -63,7 +63,6 @@ #' GET(handle = google, path = "search") GET <- function(url = NULL, config = list(), ..., handle = NULL) { hu <- handle_url(handle, url, ...) - config <- make_config(config, ...) - - make_request("get", hu$handle, hu$url, config) + req <- request_build("GET", hu$url, as.request(config), ...) + request_perform(req, hu$handle$handle) } diff --git a/R/http-head.r b/R/http-head.r index ada18d7a..7f94f9de 100644 --- a/R/http-head.r +++ b/R/http-head.r @@ -21,10 +21,9 @@ #' @export #' @examples #' HEAD("http://google.com") -#' HEAD("http://google.com")$headers +#' headers(HEAD("http://google.com")) HEAD <- function(url = NULL, config = list(), ..., handle = NULL) { hu <- handle_url(handle, url, ...) - config <- make_config(config, ..., list(nobody = TRUE)) - - make_request("head", hu$handle, hu$url, config) + req <- request_build("HEAD", hu$url, config, ..., config(nobody = TRUE)) + request_perform(req, hu$handle$handle) } diff --git a/R/http-patch.r b/R/http-patch.r index 3d0ecc03..6680f79a 100644 --- a/R/http-patch.r +++ b/R/http-patch.r @@ -15,7 +15,6 @@ PATCH <- function(url = NULL, config = list(), ..., body = NULL, encode <- match.arg(encode) hu <- handle_url(handle, url, ...) - config <- make_config(config, ...) - - make_request("patch", hu$handle, hu$url, config, body_config(body, encode)) + req <- request_build("PATCH", hu$url, body_config(body, encode), config, ...) + request_perform(req, hu$handle$handle) } diff --git a/R/http-post.r b/R/http-post.r index bceaf007..8c7943e4 100644 --- a/R/http-post.r +++ b/R/http-post.r @@ -42,7 +42,6 @@ POST <- function(url = NULL, config = list(), ..., body = NULL, encode = c("multipart", "form", "json"), multipart = TRUE, handle = NULL) { - if (!missing(multipart)) { warning("multipart is deprecated, please use encode argument instead", call. = FALSE) @@ -51,8 +50,6 @@ POST <- function(url = NULL, config = list(), ..., body = NULL, encode <- match.arg(encode) hu <- handle_url(handle, url, ...) - config <- make_config(config, ...) - - make_request("post", hu$handle, hu$url, config, - body_config(body, encode)) + req <- request_build("POST", hu$url, body_config(body, encode), config, ...) + request_perform(req, hu$handle$handle) } diff --git a/R/http-put.r b/R/http-put.r index cc9204e9..143dc501 100644 --- a/R/http-put.r +++ b/R/http-put.r @@ -14,7 +14,6 @@ PUT <- function(url = NULL, config = list(), ..., body = NULL, encode = c("multipart", "form", "json"), multipart = TRUE, handle = NULL) { - if (!missing(multipart)) { warning("multipart is deprecated, please use encode argument instead", call. = FALSE) @@ -23,7 +22,6 @@ PUT <- function(url = NULL, config = list(), ..., body = NULL, encode <- match.arg(encode) hu <- handle_url(handle, url, ...) - config <- make_config(config, ...) - - make_request("put", hu$handle, hu$url, config, body_config(body, encode)) + req <- request_build("PUT", hu$url, body_config(body, encode), config, ...) + request_perform(req, hu$handle$handle) } diff --git a/R/http-verb.R b/R/http-verb.R index 50c17c38..adb9f5dc 100644 --- a/R/http-verb.R +++ b/R/http-verb.R @@ -13,7 +13,6 @@ #' content(r) VERB <- function(verb, url = NULL, config = list(), ..., handle = NULL) { hu <- handle_url(handle, url, ...) - config <- make_config(config, ...) - - make_request(verb, hu$handle, hu$url, config) + req <- request_build(verb, hu$url, config, ...) + request_perform(req, hu$handle$handle) } diff --git a/R/oauth-signature.r b/R/oauth-signature.r index 6724b466..cecdda6e 100644 --- a/R/oauth-signature.r +++ b/R/oauth-signature.r @@ -16,7 +16,7 @@ sign_oauth1.0 <- function(app, token = NULL, token_secret = NULL, credentials <- list(oauth_token = token, oauth_token_secret = token_secret) token <- Token1.0$new(endpoint = NULL, params = params, app = app, credentials = credentials) - config(token = token) + request(auth_token = token) } #' @export diff --git a/R/oauth-token.r b/R/oauth-token.r index 1487a8be..91cd9258 100644 --- a/R/oauth-token.r +++ b/R/oauth-token.r @@ -159,11 +159,11 @@ Token1.0 <- R6::R6Class("Token1.0", inherit = Token, list( oauth <- oauth_signature(url, method, self$app, self$credentials$oauth_token, self$credentials$oauth_token_secret) if (isTRUE(self$params$as_header)) { - list(url = url, config = oauth_header(oauth)) + c(request(url = url), oauth_header(oauth)) } else { url <- parse_url(url) url$query <- c(url$query, oauth) - list(url = build_url(url), config = config()) + request(url = build_url(url)) } } )) @@ -214,14 +214,13 @@ Token2.0 <- R6::R6Class("Token2.0", inherit = Token, list( }, sign = function(method, url) { if (self$params$as_header) { - config <- add_headers( - Authorization = paste('Bearer', self$credentials$access_token) + request(url = url, headers = c( + Authorization = paste('Bearer', self$credentials$access_token)) ) - list(url = url, config = config) } else { url <- parse_url(url) url$query$access_token <- self$credentials$access_token - list(url = build_url(url), config = config()) + request(url = build_url(url)) } }, validate = function() { diff --git a/R/perform.R b/R/perform.R deleted file mode 100644 index 94f1f370..00000000 --- a/R/perform.R +++ /dev/null @@ -1,104 +0,0 @@ -# Abstract over the differences in RCurl API depending on whether or not -# you send a body. -perform <- function(handle, writer, method, opts, body) { - # Cache exact request so it can easily be replayed. - request <- list( - handle = handle, - writer = writer, - method = toupper(method), - opts = opts, - body = body - ) - - headers <- character() - add_header <- function(text) { - headers <<- c(headers, text) - nchar(text, "bytes") - } - opts$headerfunction <- add_header - - writer <- write_init(writer) - opts <- modifyList(opts, write_opts(writer)) - - opts <- modify_config(body$config, opts) - # Ensure config always gets reset - on.exit(reset_handle_config(handle, opts), add = TRUE) - curl_opts <- RCurl::curlSetOpt(curl = NULL, .opts = opts) - - if (isTRUE(body$curl_post)) { - RCurl::.postForm(handle$handle, curl_opts, body$body, body$style) - # Reset curl options that RCurl sets - RCurl::curlSetOpt(httppost = NULL, post = NULL, postfields = NULL, - curl = handle$handle) - } else { - RCurl::curlPerform(curl = handle$handle, .opts = curl_opts$values) - } - - headers <- parse_headers(headers) - content <- write_term(writer) - - if (!is.null(headers$date)) { - date <- parse_http_date(headers$Date) - } else { - date <- Sys.time() - } - - response( - url = last_request(handle)$effective.url, - status_code = last(headers)$status, - headers = last(headers)$headers, - all_headers = headers, - cookies = cookies(handle), - content = content, - date = date, - times = request_times(handle), - request = request - ) -} - - - -# http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html - -# Parses a header lines as recieved from libcurl. Multiple responses -# will be intermingled, each separated by an http status line. -parse_headers <- function(lines) { - lines <- gsub("\r?\n$", "", lines) - - new_response <- grepl("^HTTP", lines) - grps <- cumsum(new_response) - - lapply(unname(split(lines, grps)), parse_single_header) -} - -parse_single_header <- function(lines) { - status <- parse_http_status(lines[[1]]) - - # Parse headers into name-value pairs - header_lines <- lines[lines != ""][-1] - pos <- regexec("^([^:]*):\\s*(.*)$", header_lines) - pieces <- regmatches(header_lines, pos) - - n <- vapply(pieces, length, integer(1)) - if (any(n != 3)) { - bad <- header_lines[n != 3] - pieces <- pieces[n == 3] - - warning("Failed to parse headers:\n", paste0(bad, "\n"), call. = FALSE) - } - - names <- vapply(pieces, "[[", 2, FUN.VALUE = character(1)) - values <- lapply(pieces, "[[", 3) - headers <- insensitive(stats::setNames(values, names)) - - list(status = status$status, version = status$version, headers = headers) -} - -parse_http_status <- function(x) { - status <- as.list(strsplit(x, "\\s+")[[1]]) - names(status) <- c("version", "status", "message")[seq_along(status)] - status$status <- as.integer(status$status) - - - status -} diff --git a/R/progress.R b/R/progress.R index 8392b52f..c51a67b7 100644 --- a/R/progress.R +++ b/R/progress.R @@ -13,7 +13,10 @@ progress <- function(type = c("down", "up")) { type <- match.arg(type) - config(noprogress = FALSE, progressfunction = progress_bar(type)) + request(options = list( + noprogress = FALSE, + progressfunction = progress_bar(type) + )) } progress_bar <- function(type) { @@ -53,7 +56,7 @@ progress_bar <- function(type) { 0L } - safe_callback(show_progress) + show_progress } diff --git a/R/request.R b/R/request.R new file mode 100644 index 00000000..5ddd86fc --- /dev/null +++ b/R/request.R @@ -0,0 +1,157 @@ +request <- function(method = NULL, url = NULL, headers = NULL, + fields = NULL, options = NULL, auth_token = NULL, + output = NULL) { + if (!is.null(method)) + stopifnot(is.character(method), length(method) == 1) + if (!is.null(url)) + stopifnot(is.character(url), length(url) == 1) + if (!is.null(headers)) + stopifnot(is.character(headers)) + if (!is.null(fields)) + stopifnot(is.list(fields)) + if (!is.null(output)) + stopifnot(inherits(output, "write_function")) + + structure( + list( + method = method, + url = url, + headers = keep_last(headers), + fields = fields, + options = compact(keep_last(options)), + auth_token = auth_token, + output = output + ), + class = "request" + ) +} +is.request <- function(x) inherits(x, "request") + +request_default <- function() { + c( + request( + options = list( + useragent = default_ua(), + cainfo = find_cert_bundle() + ), + headers = c(Accept = "application/json, text/xml, application/xml, */*"), + output = write_function("write_memory") + ), + getOption("httr_config") + ) +} + +#' @export +c.request <- function(...) { + Reduce(request_combine, list(...)) +} + +as.request <- function(x) UseMethod("as.request") +as.request.list <- function(x) structure(x, class = "request") +as.request.request <- function(x) x +as.request.NULL <- function(x) request() +as.request.Token <- function(x) request(auth_token = x) + +request_build <- function(method, url, ...) { + extra <- list(...) + extra[has_names(extra)] <- NULL + + req <- Reduce(request_combine, extra, init = request()) + + req$method <- method + req$url <- url + + req +} + +request_combine <- function(x, y) { + if (length(x) == 0 && length(y) == 0) return(request()) + if (length(x) == 0) return(y) + if (length(y) == 0) return(x) + stopifnot(is.request(x), is.request(y)) + + request( + method = y$method %||% x$method, + url = y$url %||% x$url, + headers = keep_last(x$headers, y$headers), + fields = c(x$fields, y$fields), + options = keep_last(x$options, y$options), + auth_token = y$auth_token %||% x$auth_token, + output = y$output %||% x$output + ) +} + +#' @export +print.request <- function(x, ...) { + cat("\n") + if (!is.null(x$method) && !is.null(x$url)) + cat(toupper(x$method), " ", x$url, "\n", sep = "") + if (!is.null(x$output)) + cat("Output: ", class(x$output)[[1]], "\n", sep = "") + named_vector("Options", x$options) + named_vector("Headers", x$headers) + named_vector("Fields", x$fields) + if (!is.null(x$auth_token)) { + cat("Auth token: ", class(x$auth_token)[[1]], "\n", sep = "") + } + +} + +request_prepare <- function(req) { + req <- request_combine(request_default(), req) + + if (req$method != "POST") + req$options$customrequest <- req$method + + # Sign request, if needed + token <- req$auth_token + if (!is.null(token)) + req <- c(req, token$sign(req$method, req$url)) + + + req +} + +request_perform <- function(req, handle, refresh = TRUE) { + stopifnot(is.request(req), inherits(handle, "curl_handle")) + req <- request_prepare(req) + + curl::handle_setopt(handle, .list = req$options) + if (!is.null(req$fields)) + curl::handle_setform(handle, .list = req$fields) + curl::handle_setheaders(handle, .list = req$headers) + on.exit(curl::handle_reset(handle), add = TRUE) + + resp <- request_fetch(req$output, req$url, handle) + + # If return 401 and have auth token, refresh it and then try again + needs_refresh <- refresh && resp$status_code == 401L && + !is.null(req$auth_token) && req$auth_token$can_refresh() + if (needs_refresh) { + message("Auto-refreshing stale OAuth token.") + req$auth_token$refresh() + return(request_perform(req, handle, refresh = FALSE)) + } + + all_headers <- parse_headers(resp$headers) + headers <- last(all_headers)$headers + if (!is.null(headers$date)) { + date <- parse_http_date(headers$Date) + } else { + date <- Sys.time() + } + + response( + url = resp$url, + status_code = resp$status_code, + headers = headers, + all_headers = all_headers, + cookies = curl::handle_cookies(handle), + content = resp$content, + date = date, + times = resp$times, + request = req, + handle = handle + ) +} + diff --git a/R/safe-callback.R b/R/safe-callback.R index cdd367cd..8969b995 100644 --- a/R/safe-callback.R +++ b/R/safe-callback.R @@ -1,31 +1,10 @@ #' Generate a safe R callback. #' -#' Whenever an R callback function is passed to Rcurl, it needs to be wrapped -#' in this handler which converts errors and interrupts to the appropriate -#' values that cause RCurl to terminate a request -#' #' @param f A function. +#' @keywords deprecated #' @export -#' @examples -#' f1 <- function(x) { -#' if (x < 0) stop("Negative value") -#' sqrt(x) -#' } -#' f2 <- safe_callback(f1) -#' f2(-10) safe_callback <- function(f) { - force(f) - - function(...) { - tryCatch(f(...), - error = function(e, ...) { - message("Error:", e$message) - 1L - }, - interrupt = function(...) { - message("Interrupted by user") - 1L - } - ) - } + warning("`safe_callback()` is no longer needed and will be removed in a ", + "future version", call. = FALSE) + f } diff --git a/R/timeout.r b/R/timeout.r index 38f7306b..dacd4e0e 100644 --- a/R/timeout.r +++ b/R/timeout.r @@ -14,5 +14,5 @@ timeout <- function(seconds) { stop("Timeout cannot be less than 1 ms", call. = FALSE) } - config(timeout.ms = seconds * 1000) + config(timeout_ms = seconds * 1000) } diff --git a/R/upload-file.r b/R/upload-file.r index 41f7a07a..3f7d1d92 100644 --- a/R/upload-file.r +++ b/R/upload-file.r @@ -1,16 +1,21 @@ #' Upload a file with \code{\link{POST}} or \code{\link{PUT}}. #' -#' This is a tiny wrapper for \pkg{RCurl}'s \code{\link[RCurl]{fileUpload}}. -#' #' @param path path to file #' @param type mime type of path. If not supplied, will be guess by #' \code{\link[mime]{guess_type}} when needed. #' @export #' @examples -#' POST("http://httpbin.org/post", -#' body = list(y = upload_file(system.file("CITATION")))) +#' citation <- upload_file(system.file("CITATION")) +#' POST("http://httpbin.org/post", body = citation) +#' POST("http://httpbin.org/post", body = list(y = citation)) upload_file <- function(path, type = NULL) { - stopifnot(is.character(path), length(path) == 1) - RCurl::fileUpload(path, contentType = type) + stopifnot(is.character(path), length(path) == 1, file.exists(path)) + + if (is.null(type)) + type <- mime::guess_type(path) + + curl::form_file(path, type) } +#' @export +as.character.form_file <- function(x, ...) x diff --git a/R/url-query.r b/R/url-query.r index 934a606c..a7773ad2 100644 --- a/R/url-query.r +++ b/R/url-query.r @@ -2,8 +2,8 @@ parse_query <- function(query) { params <- vapply(strsplit(query, "&")[[1]], str_split_fixed, "=", 2, FUN.VALUE = character(2)) - values <- as.list(RCurl::curlUnescape(params[2, ])) - names(values) <- RCurl::curlUnescape(params[1, ]) + values <- as.list(curl::curl_unescape(params[2, ])) + names(values) <- curl::curl_unescape(params[1, ]) values } @@ -14,10 +14,10 @@ compose_query <- function(elements) { encode <- function(x) { if (inherits(x, "AsIs")) return(x) - RCurl::curlEscape(x) + curl::curl_escape(as.character(x)) } - names <- RCurl::curlEscape(names(elements)) + names <- curl::curl_escape(names(elements)) values <- vapply(elements, encode, character(1)) paste0(names, "=", values, collapse = "&") diff --git a/R/url.r b/R/url.r index 2ea61515..8189b905 100644 --- a/R/url.r +++ b/R/url.r @@ -117,8 +117,8 @@ build_url <- function(url) { if (is.list(url$query)) { url$query <- compact(url$query) - names <- RCurl::curlEscape(names(url$query)) - values <- RCurl::curlEscape(url$query) + names <- curl::curl_escape(names(url$query)) + values <- curl::curl_escape(as.character(url$query)) query <- paste0(names, "=", values, collapse = "&") } else { diff --git a/R/utils.r b/R/utils.r index c4a2374c..57ae0835 100644 --- a/R/utils.r +++ b/R/utils.r @@ -6,17 +6,13 @@ timestamp <- function(x = Sys.time()) { format(x, "%Y-%m-%dT%H:%M:%SZ", tz = "UTC") } -sort_names <- function(x) x[order(names(x))] +sort_names <- function(x) x[order(names(x))] nonce <- function(length = 10) { paste(sample(c(letters, LETTERS, 0:9), length, replace = TRUE), collapse = "") } -curl_version <- function() { - as.numeric_version(RCurl::curlVersion()$version) -} - has_env_var <- function(x) !identical(Sys.getenv(x), "") named <- function(x) x[has_names(x)] @@ -26,7 +22,7 @@ has_names <- function(x) { nms <- names(x) if (is.null(nms)) return(rep(FALSE, length(x))) - names(x) != "" + !is.na(names(x)) & names(x) != "" } travis_encrypt <- function(vars) { @@ -53,3 +49,33 @@ compact <- function(x) { null <- vapply(x, is.null, logical(1)) x[!null] } + +keep_last <- function(...) { + x <- c(...) + x[!duplicated(names(x), fromLast = TRUE)] +} + +named_vector <- function(title, x) { + if (length(x) == 0) return() + + cat(title, ":\n", sep = "") + bullets <- paste0("* ", names(x), ": ", as.character(x)) + cat(bullets, sep = "\n") +} + +keep_last <- function(...) { + x <- c(...) + x[!duplicated(names(x), fromLast = TRUE)] +} + +find_cert_bundle <- function() { + if (.Platform$OS.type != "windows") + return() + + env <- Sys.getenv("CURL_CA_BUNDLE") + if (!identical(env, "")) { + env + } else { + system.file("cacert.pem", package = "httr") + } +} diff --git a/R/verbose.r b/R/verbose.r index 3c938ebd..27fc7f7d 100644 --- a/R/verbose.r +++ b/R/verbose.r @@ -51,7 +51,7 @@ #' POST_verbose("") #' POST_verbose("xyz") verbose <- function(data_out = TRUE, data_in = FALSE, info = FALSE, ssl = FALSE) { - debug <- function(msg, type, curl) { + debug <- function(type, msg) { switch(type + 1, text = if (info) prefix_message("* ", msg), headerIn = prefix_message("<- ", msg), @@ -59,14 +59,15 @@ verbose <- function(data_out = TRUE, data_in = FALSE, info = FALSE, ssl = FALSE) dataIn = if (data_in) prefix_message("<< ", msg, TRUE), dataOut = if (data_out) prefix_message(">> ", msg, TRUE), sslDataIn = if (data_in && ssl) prefix_message("*< ", msg, TRUE), - sslDataOut = if (data_out && ssl) prefix_message("*> ", msg, TRUE), + sslDataOut = if (data_out && ssl) prefix_message("*> ", msg, TRUE) ) - 0 } - config(debugfunction = safe_callback(debug), verbose = TRUE) + config(debugfunction = debug, verbose = TRUE) } prefix_message <- function(prefix, x, blank_line = FALSE) { + x <- readBin(x, character()) + lines <- unlist(strsplit(x, "\n", fixed = TRUE, useBytes = TRUE)) out <- paste0(prefix, lines, collapse = "\n") message(out) diff --git a/R/write-function.R b/R/write-function.R index ddf65537..38e163b0 100644 --- a/R/write-function.R +++ b/R/write-function.R @@ -2,14 +2,6 @@ #' #' This S3 object allows you to control how the response body is saved. #' -#' There are three key methods: -#' \itemize{ -#' \item \code{write_init()}: called before the write is started. It should -#' return a modified object. -#' \item \code{write_opts()}: returns a list options passed on to RCurl -#' \item \code{write_term()}: called after the request is complete. -#' Should return the content (or a pointer to it) -#' } #' @param subclass,... Class name and fields. Used in class constructors. #' @param x A \code{write_function} object to process. #' @keywords internal @@ -17,17 +9,6 @@ write_function <- function(subclass, ...) { structure(list(...), class = c(subclass, "write_function")) } -#' @export -#' @rdname write_function -write_init <- function(x) UseMethod("write_init") -#' @export -#' @rdname write_function -write_opts <- function(x) UseMethod("write_opts") -#' @export -#' @rdname write_function -write_term <- function(x) UseMethod("write_term") - -# Disk ------------------------------------------------------------------------- #' Control where the response body is written. #' @@ -39,7 +20,6 @@ write_term <- function(x) UseMethod("write_term") #' @param path Path to content to. #' @param overwrite Will only overwrite existing \code{path} if TRUE. #' @export -#' @useDynLib httr writer #' @examples #' tmp <- tempfile() #' r1 <- GET("https://www.google.com", write_disk(tmp)) @@ -57,68 +37,13 @@ write_disk <- function(path, overwrite = FALSE) { if (!overwrite && file.exists(path)) { stop("Path exists and overwrite is FALSE", call. = FALSE) } - config( - writer = write_function("write_disk", path = path, file = NULL) - ) -} -#' @export -write_init.write_disk <- function(x) { - x$file <- RCurl::CFILE(x$path, "wb") - x -} -#' @export -write_opts.write_disk <- function(x) { - list( - writefunction = writer$address, - writedata = x$file@ref - ) -} -#' @export -#' @useDynLib httr close_file -write_term.write_disk <- function(x) { - .Call(close_file, x$file@ref) - x$file <- NULL - path(x$path) -} -#' @export -print.write_disk <- function(x, ...) { - cat(" ", x$path, "\n", sep = "") + request(output = write_function("write_disk", path = path, file = NULL)) } -path <- function(x) structure(x, class = "path") -#' @export -length.path <- function(x) file.info(x)$size -is.path <- function(x) inherits(x, "path") - -# Memory ----------------------------------------------------------------------- - #' @rdname write_disk #' @export write_memory <- function() { - config( - writer = write_function("write_memory", buffer = NULL) - ) -} -#' @export -print.write_memory <- function(x, ...) { - cat("\n") -} - -#' @export -write_init.write_memory <- function(x) { - x$buffer <- RCurl::binaryBuffer() - x -} -#' @export -write_opts.write_memory <- function(x) { - list( - writefunction = getNativeSymbolInfo("R_curl_write_binary_data")$address, - writedata = x$buffer@ref - ) -} -#' @export -write_term.write_memory <- function(x) { - methods::as(x$buffer, "raw") + request(output = write_function("write_memory")) } # Streaming ----------------------------------------------------------------------- @@ -131,37 +56,36 @@ write_term.write_memory <- function(x) { #' #' @param f Callback function. It should have a single argument, a raw #' vector containing the bytes recieved from the server. This will usually -#' be 16k or less +#' be 16k or less. It should return the length of bytes processed - if +#' this is less than the input length, the function will terminate. #' @examples #' GET("https://jeroenooms.github.io/data/diamonds.json", -#' write_stream(function(x) print(length(x))) +#' write_stream(function(x) { +#' print(length(x)) +#' length(x) +#' }) #' ) #' @export write_stream <- function(f) { stopifnot(is.function(f), length(formals(f)) == 1) - - config( - writer = write_function("write_stream", f = safe_callback(f)) - ) -} -#' @export -print.write_stream <- function(x, ...) { - cat("\n") + request(output = write_function("write_stream", f = f)) } -#' @export -write_init.write_stream <- function(x) { - x + +request_fetch <- function(x, url, handle) UseMethod("request_fetch") +request_fetch.write_memory <- function(x, url, handle) { + curl::curl_fetch_memory(url, handle = handle) } -#' @export -#' @useDynLib httr write_callback -write_opts.write_stream <- function(x) { - list( - writefunction = write_callback$address, - writedata = x$f - ) +request_fetch.write_disk <- function(x, url, handle) { + resp <- curl::curl_fetch_disk(url, x$path, handle = handle) + resp$content <- path(resp$content) + resp } -#' @export -write_term.write_stream <- function(x) { - raw() +request_fetch.write_stream <- function(x, url, handle) { + curl::curl_fetch_stream(url, x$f, handle = handle) } + +path <- function(x) structure(x, class = "path") +#' @export +length.path <- function(x) file.info(x)$size +is.path <- function(x) inherits(x, "path") diff --git a/man/HEAD.Rd b/man/HEAD.Rd index 7988eead..1dedd981 100644 --- a/man/HEAD.Rd +++ b/man/HEAD.Rd @@ -48,7 +48,7 @@ cache MUST treat the cache entry as stale. } \examples{ HEAD("http://google.com") -HEAD("http://google.com")$headers +headers(HEAD("http://google.com")) } \seealso{ Other http methods: \code{\link{BROWSE}}; diff --git a/man/config.Rd b/man/config.Rd index f821602e..c0c488b8 100644 --- a/man/config.Rd +++ b/man/config.Rd @@ -4,10 +4,12 @@ \alias{config} \title{Set curl options.} \usage{ -config(...) +config(..., token = NULL) } \arguments{ \item{...}{named Curl options.} + +\item{token}{An OAuth token (1.0 or 2.0)} } \description{ Generally you should only need to use this function to set CURL options diff --git a/man/handle.Rd b/man/handle.Rd index 9d8732e8..f7bf0497 100644 --- a/man/handle.Rd +++ b/man/handle.Rd @@ -9,8 +9,7 @@ handle(url, cookies = TRUE) \arguments{ \item{url}{full url to site} -\item{cookies}{if \code{TRUE} (the default), maintain cookies across -requests.} +\item{cookies}{DEPRECATED} } \description{ This handle preserves settings and cookies across multiple requests. It is diff --git a/man/brew_dr.Rd b/man/httr_dr.Rd similarity index 83% rename from man/brew_dr.Rd rename to man/httr_dr.Rd index fdc147d6..8c753350 100644 --- a/man/brew_dr.Rd +++ b/man/httr_dr.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2 (4.1.1): do not edit by hand % Please edit documentation in R/doctor.R -\name{brew_dr} -\alias{brew_dr} +\name{httr_dr} +\alias{httr_dr} \title{Diagnose common configuration problems} \usage{ -brew_dr() +httr_dr() } \description{ Currently one check: that curl uses nss. diff --git a/man/init_oauth1.0.Rd b/man/init_oauth1.0.Rd index 80674c4f..40874312 100644 --- a/man/init_oauth1.0.Rd +++ b/man/init_oauth1.0.Rd @@ -5,7 +5,7 @@ \title{Retrieve OAuth 1.0 access token.} \usage{ init_oauth1.0(endpoint, app, permission = NULL, - is_interactive = interactive()) + is_interactive = interactive(), host = "127.0.0.1", port = 1410) } \arguments{ \item{endpoint}{An OAuth endpoint, created by \code{\link{oauth_endpoint}}} @@ -16,6 +16,10 @@ init_oauth1.0(endpoint, app, permission = NULL, \item{permission}{optional, a string of permissions to ask for.} \item{is_interactive}{Is the current environment interactive?} + +\item{host}{ip address for the listener} + +\item{port}{for the listener} } \description{ See demos for use. diff --git a/man/init_oauth2.0.Rd b/man/init_oauth2.0.Rd index 8e771a88..56db0c63 100644 --- a/man/init_oauth2.0.Rd +++ b/man/init_oauth2.0.Rd @@ -5,7 +5,8 @@ \title{Retrieve OAuth 2.0 access token.} \usage{ init_oauth2.0(endpoint, app, scope = NULL, type = NULL, - use_oob = getOption("httr_oob_default"), is_interactive = interactive()) + use_oob = getOption("httr_oob_default"), is_interactive = interactive(), + host = "127.0.0.1", port = 1410) } \arguments{ \item{endpoint}{An OAuth endpoint, created by \code{\link{oauth_endpoint}}} @@ -23,6 +24,10 @@ code. Defaults to the of the \code{"httr_oob_default"} default, or \code{TRUE} if \code{httpuv} is not installed.} \item{is_interactive}{Is the current environment interactive?} + +\item{host}{ip address for the listener} + +\item{port}{for the listener} } \description{ See demos for use. diff --git a/man/oauth1.0_token.Rd b/man/oauth1.0_token.Rd index 73d3577a..87499a30 100644 --- a/man/oauth1.0_token.Rd +++ b/man/oauth1.0_token.Rd @@ -5,7 +5,7 @@ \title{Generate an oauth1.0 token.} \usage{ oauth1.0_token(endpoint, app, permission = NULL, as_header = TRUE, - cache = getOption("httr_oauth_cache")) + cache = getOption("httr_oauth_cache"), host = "127.0.0.1", port = 1410) } \arguments{ \item{endpoint}{An OAuth endpoint, created by \code{\link{oauth_endpoint}}} @@ -22,6 +22,10 @@ If \code{FALSE}, adds as parameter to url.} using the default cache file \code{.oauth-httr}, \code{FALSE} means don't cache, and \code{NA} means to guess using some sensible heuristics. A string mean use the specified path as the cache file.} + +\item{host}{ip address for the listener} + +\item{port}{for the listener} } \value{ A \code{Token1.0} reference class (RC) object. diff --git a/man/oauth2.0_token.Rd b/man/oauth2.0_token.Rd index 4fbd2586..2b213e54 100644 --- a/man/oauth2.0_token.Rd +++ b/man/oauth2.0_token.Rd @@ -6,7 +6,7 @@ \usage{ oauth2.0_token(endpoint, app, scope = NULL, type = NULL, use_oob = getOption("httr_oob_default"), as_header = TRUE, - cache = getOption("httr_oauth_cache")) + cache = getOption("httr_oauth_cache"), host = "127.0.0.1", port = 1410) } \arguments{ \item{endpoint}{An OAuth endpoint, created by \code{\link{oauth_endpoint}}} @@ -30,6 +30,10 @@ If \code{FALSE}, adds as parameter to url.} using the default cache file \code{.oauth-httr}, \code{FALSE} means don't cache, and \code{NA} means to guess using some sensible heuristics. A string mean use the specified path as the cache file.} + +\item{host}{ip address for the listener} + +\item{port}{for the listener} } \value{ A \code{Token2.0} reference class (RC) object. diff --git a/man/oauth_listener.Rd b/man/oauth_listener.Rd index 94a07741..690b5c97 100644 --- a/man/oauth_listener.Rd +++ b/man/oauth_listener.Rd @@ -4,12 +4,17 @@ \alias{oauth_listener} \title{Create a webserver to listen for OAuth callback.} \usage{ -oauth_listener(request_url, is_interactive = interactive()) +oauth_listener(request_url, is_interactive = interactive(), + host = "127.0.0.1", port = 1410) } \arguments{ \item{request_url}{the url to send the browser to} \item{is_interactive}{Is an interactive environment available?} + +\item{host}{ip address for the listener} + +\item{port}{for the listener} } \description{ This opens a web browser pointing to \code{request_url}, and opens a diff --git a/man/safe_callback.Rd b/man/safe_callback.Rd index 5f46f43e..acd5be5f 100644 --- a/man/safe_callback.Rd +++ b/man/safe_callback.Rd @@ -10,16 +10,7 @@ safe_callback(f) \item{f}{A function.} } \description{ -Whenever an R callback function is passed to Rcurl, it needs to be wrapped -in this handler which converts errors and interrupts to the appropriate -values that cause RCurl to terminate a request -} -\examples{ -f1 <- function(x) { - if (x < 0) stop("Negative value") - sqrt(x) -} -f2 <- safe_callback(f1) -f2(-10) +Generate a safe R callback. } +\keyword{deprecated} diff --git a/man/upload_file.Rd b/man/upload_file.Rd index 8b8b405a..e3ccb2da 100644 --- a/man/upload_file.Rd +++ b/man/upload_file.Rd @@ -13,10 +13,11 @@ upload_file(path, type = NULL) \code{\link[mime]{guess_type}} when needed.} } \description{ -This is a tiny wrapper for \pkg{RCurl}'s \code{\link[RCurl]{fileUpload}}. +Upload a file with \code{\link{POST}} or \code{\link{PUT}}. } \examples{ -POST("http://httpbin.org/post", - body = list(y = upload_file(system.file("CITATION")))) +citation <- upload_file(system.file("CITATION")) +POST("http://httpbin.org/post", body = citation) +POST("http://httpbin.org/post", body = list(y = citation)) } diff --git a/man/write_function.Rd b/man/write_function.Rd index 3294e706..2b0785ac 100644 --- a/man/write_function.Rd +++ b/man/write_function.Rd @@ -2,18 +2,9 @@ % Please edit documentation in R/write-function.R \name{write_function} \alias{write_function} -\alias{write_init} -\alias{write_opts} -\alias{write_term} \title{S3 object to define response writer.} \usage{ write_function(subclass, ...) - -write_init(x) - -write_opts(x) - -write_term(x) } \arguments{ \item{subclass,...}{Class name and fields. Used in class constructors.} @@ -23,15 +14,5 @@ write_term(x) \description{ This S3 object allows you to control how the response body is saved. } -\details{ -There are three key methods: -\itemize{ - \item \code{write_init()}: called before the write is started. It should - return a modified object. - \item \code{write_opts()}: returns a list options passed on to RCurl - \item \code{write_term()}: called after the request is complete. - Should return the content (or a pointer to it) -} -} \keyword{internal} diff --git a/man/write_stream.Rd b/man/write_stream.Rd index cd1729a5..d2863f0f 100644 --- a/man/write_stream.Rd +++ b/man/write_stream.Rd @@ -9,7 +9,8 @@ write_stream(f) \arguments{ \item{f}{Callback function. It should have a single argument, a raw vector containing the bytes recieved from the server. This will usually -be 16k or less} +be 16k or less. It should return the length of bytes processed - if +this is less than the input length, the function will terminate.} } \description{ This is the most general way of processing the response from the server - @@ -18,7 +19,10 @@ with them. } \examples{ GET("https://jeroenooms.github.io/data/diamonds.json", - write_stream(function(x) print(length(x))) + write_stream(function(x) { + print(length(x)) + length(x) + }) ) } diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 3521f270..00000000 --- a/src/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.so -*.dll -*.o \ No newline at end of file diff --git a/src/writer.c b/src/writer.c deleted file mode 100644 index e742707e..00000000 --- a/src/writer.c +++ /dev/null @@ -1,47 +0,0 @@ -#include -#include -#include - -// From http://stackoverflow.com/questions/17329288/ -// License: http://creativecommons.org/licenses/by-sa/3.0/ -// Author: Ast Derek, https://github.com/AstDerek -size_t writer(void *buffer, size_t size, size_t nmemb, void* stream) { - fwrite(buffer, size, nmemb, (FILE *) stream); - return size * nmemb; -} - -// From: RCurl -// BSD_3_clause -// YEAR: 2001-2014 -// COPYRIGHT HOLDER: Duncan Temple Lang -// ORGANIZATION: Bell Labs, Lucent Technologies; University of California -void close_file(SEXP r_file) { - FILE *f = (FILE *) R_ExternalPtrAddr(r_file); - if (f) { - fflush(f); - fclose(f); - R_SetExternalPtrAddr(r_file, NULL); - } -} - - -// Adapted from RCurl -// BSD_3_clause -// YEAR: 2001-2014 -// COPYRIGHT HOLDER: Duncan Temple Lang -// ORGANIZATION: Bell Labs, Lucent Technologies; University of California -size_t write_callback(void *buffer, size_t size, size_t nmemb, void* fun) { - if (TYPEOF(fun) != CLOSXP) return 0; - - // Convert buffer into a raw vector - SEXP bytes = PROTECT(allocVector(RAWSXP, size * nmemb)); - memcpy(RAW(bytes), buffer, size * nmemb); - - // Call fun with bytes as first argument - SEXP call = PROTECT(LCONS(fun, LCONS(bytes, R_NilValue))); - Rf_eval(call, R_GlobalEnv); - UNPROTECT(2); - - return size * nmemb; -} - diff --git a/tests/testthat/test-config.r b/tests/testthat/test-config.r index f1a93182..1e3b5407 100644 --- a/tests/testthat/test-config.r +++ b/tests/testthat/test-config.r @@ -1,9 +1,5 @@ context("Config") -test_that("timeout enforced", { - skip_on_cran() - expect_error(GET("http://httpbin.org/delay/1", timeout(0.5)), "timed out") -}) test_that("basic authentication works", { h <- handle("http://httpbin.org") @@ -33,82 +29,9 @@ test_that("digest authentication works", { expect_equal(r$status_code, 200) }) -test_that("oauth2.0 signing works", { - request_url <- "http://httpbin.org/headers" - - token <- Token2.0$new( - app = oauth_app("x", "y", "z"), - endpoint = oauth_endpoints("google"), - credentials = list(access_token = "ofNoArms") - ) - - token$params$as_header <- TRUE - header_response <- GET(request_url, config(token = token)) - response_content <- content(header_response)$headers - expect_equal("Bearer ofNoArms", response_content$Authorization) - expect_equal(request_url, header_response$url) - - token$params$as_header <- FALSE - url_response <- GET(request_url, config(token = token)) - response_content <- content(url_response)$headers - expect_equal(NULL, response_content$Authorization) - expect_equal( - parse_url(url_response$url)$query, - list(access_token = "ofNoArms") - ) -}) - -test_that("partial OAuth1 flow works", { +test_that("timeout enforced", { skip_on_cran() - # From rfigshare - - endpoint <- oauth_endpoint( - base_url = "http://api.figshare.com/v1/pbl/oauth", - "request_token", "authorize", "access_token" - ) - myapp <- oauth_app("rfigshare", - key = "Kazwg91wCdBB9ggypFVVJg", - secret = "izgO06p1ymfgZTsdsZQbcA") - sig <- sign_oauth1.0(myapp, - token = "xdBjcKOiunwjiovwkfTF2QjGhROeLMw0y0nSCSgvg3YQxdBjcKOiunwjiovwkfTF2Q", - token_secret = "4mdM3pfekNGO16X4hsvZdg") - - r <- GET("http://api.figshare.com/v1/my_data", sig) - expect_equal(status_code(r), 200) -}) - - -# Construction ---------------------------------------------------------------- - -test_that("c.config overwrites repeated options", { - expect_equal(c(config(url = "a"), config(url = "b")) , - config(url = "b")) -}) - -test_that("c.config merges headers", { - expect_equal(c(config(httpheader = c("a" = "a")), config(httpheader = c("b" = "b"))), - config(httpheader = c("a" = "a", "b" = "b"))) -}) - -test_that("config() keeps last of repeated options", { - expect_equal(config(url = "a", url = "b"), config(url = "b")) -}) - -test_that("config() merges headers", { - expect_equal(config(httpheader = c("a" = "a"), httpheader = c("b" = "b")), - config(httpheader = c("a" = "a", "b" = "b"))) + expect_error(GET("http://httpbin.org/delay/1", timeout(0.5)), + "Timeout was reached") }) -test_that("make_config combines headers correctly", { - config <- make_config(list(), add_headers(a = 1), add_headers(a = 2)) - expect_is(config, "config") - expect_equal(config$httpheader, c(a = "2")) - - config <- make_config(add_headers(a = 1), add_headers(a = 2)) - expect_is(config, "config") - expect_equal(config$httpheader, c(a = "2")) - - config <- make_config(add_headers(a = 1), list(httpheader = c(a = "2"))) - expect_is(config, "config") - expect_equal(config$httpheader, c(a = "2")) -}) diff --git a/tests/testthat/test-header.r b/tests/testthat/test-header.r index 9146eb2f..cd0ea987 100644 --- a/tests/testthat/test-header.r +++ b/tests/testthat/test-header.r @@ -3,17 +3,12 @@ context("Headers") # Setting --------------------------------------------------------------------- test_that("Only last duplicated header kept in add_headers", { - expect_equal(add_headers(x = 1, x = 2)$httpheader, c(x = "2")) + expect_equal(add_headers(x = 1, x = 2)$header, c(x = "2")) }) test_that("Only last duplicated header kept when combined", { out <- c(add_headers(x = 1), add_headers(x = 2)) - expect_equal(out$httpheader, c(x = "2")) -}) - -test_that("Only last duplicated header kept when modified", { - out <- modify_config(add_headers(x = 1), add_headers(x = 2)) - expect_equal(out$httpheader, c(x = "2")) + expect_equal(out$header, c(x = "2")) }) # Getting --------------------------------------------------------------------- @@ -29,11 +24,8 @@ test_that("All headers captures headers from redirects", { # Parsing --------------------------------------------------------------------- test_that("Trailing line breaks removed", { - lines <- c( - "HTTP/1.1 200 OK", - "A: B\r\n" - ) - expect_equal(parse_headers(lines)[[1]]$headers$A, "B") + header <- charToRaw("HTTP/1.1 200 OK\r\nA: B\r\n") + expect_equal(parse_headers(header)[[1]]$headers$A, "B") }) test_that("Invalid header raises error", { @@ -42,11 +34,12 @@ test_that("Invalid header raises error", { "A: B", "Invalid" ) - expect_warning(parse_headers(lines), "Failed to parse headers") + header <- charToRaw(paste(lines, collapse = "\n")) + expect_warning(parse_headers(header), "Failed to parse headers") }) test_that("http status line only needs two components", { - headers <- parse_headers("HTTP/1.1 200") + headers <- parse_headers(charToRaw("HTTP/1.1 200")) expect_equal(headers[[1]]$status, 200L) }) @@ -57,6 +50,8 @@ test_that("Key/value parsing tolerates multiple ':'", { "A: B:C", "D:E:F" ) - expect_equal(parse_headers(lines)[[1]]$headers$A, "B:C") - expect_equal(parse_headers(lines)[[1]]$headers$D, "E:F") + header <- charToRaw(paste(lines, collapse = "\n")) + + expect_equal(parse_headers(header)[[1]]$headers$A, "B:C") + expect_equal(parse_headers(header)[[1]]$headers$D, "E:F") }) diff --git a/tests/testthat/test-oauth-listener.R b/tests/testthat/test-oauth-listener.R deleted file mode 100644 index 99f02746..00000000 --- a/tests/testthat/test-oauth-listener.R +++ /dev/null @@ -1 +0,0 @@ -context("OAuth listener") diff --git a/tests/testthat/test-oauth.R b/tests/testthat/test-oauth.R new file mode 100644 index 00000000..c2aa2391 --- /dev/null +++ b/tests/testthat/test-oauth.R @@ -0,0 +1,46 @@ +context("Oauth") + +test_that("oauth2.0 signing works", { + request_url <- "http://httpbin.org/headers" + + token <- Token2.0$new( + app = oauth_app("x", "y", "z"), + endpoint = oauth_endpoints("google"), + credentials = list(access_token = "ofNoArms") + ) + + token$params$as_header <- TRUE + header_response <- GET(request_url, config(token = token)) + response_content <- content(header_response)$headers + expect_equal("Bearer ofNoArms", response_content$Authorization) + expect_equal(request_url, header_response$url) + + token$params$as_header <- FALSE + url_response <- GET(request_url, config(token = token)) + response_content <- content(url_response)$headers + expect_equal(NULL, response_content$Authorization) + expect_equal( + parse_url(url_response$url)$query, + list(access_token = "ofNoArms") + ) +}) + +test_that("partial OAuth1 flow works", { + skip_on_cran() + # From rfigshare + + endpoint <- oauth_endpoint( + base_url = "http://api.figshare.com/v1/pbl/oauth", + "request_token", "authorize", "access_token" + ) + myapp <- oauth_app("rfigshare", + key = "Kazwg91wCdBB9ggypFVVJg", + secret = "izgO06p1ymfgZTsdsZQbcA") + sig <- sign_oauth1.0(myapp, + token = "xdBjcKOiunwjiovwkfTF2QjGhROeLMw0y0nSCSgvg3YQxdBjcKOiunwjiovwkfTF2Q", + token_secret = "4mdM3pfekNGO16X4hsvZdg") + + r <- GET("http://api.figshare.com/v1/my_data", sig) + expect_equal(status_code(r), 200) +}) + diff --git a/tests/testthat/test-request.r b/tests/testthat/test-request.r index dc6bd3e1..3146c82d 100644 --- a/tests/testthat/test-request.r +++ b/tests/testthat/test-request.r @@ -1,35 +1,11 @@ context("Request") -test_that("status codes returned as expected", { - - expect_equal(GET("http://httpbin.org/status/320")$status_code, 320) - expect_equal(GET("http://httpbin.org/status/404")$status_code, 404) - expect_equal(GET("http://httpbin.org/status/418")$status_code, 418) - -}) - -test_that("status converted to errors", { - - s200 <- GET("http://httpbin.org/status/200") - s300 <- GET("http://httpbin.org/status/300") - s404 <- GET("http://httpbin.org/status/404") - s500 <- GET("http://httpbin.org/status/500") - - expect_equal(stop_for_status(s200), TRUE) - expect_error(stop_for_status(s300), c("redirection.*\\(300\\)")) - expect_error(stop_for_status(s404), c("client.*\\(404\\)")) - expect_error(stop_for_status(s500), c("server.*\\(500\\)")) +test_that("c.request overwrites repeated options", { + expect_equal(c(request(url = "a"), request(url = "b")) , + request(url = "b")) }) -test_that("headers returned as expected", { - round_trip <- function(...) { - req <- GET("http://httpbin.org/headers", add_headers(...)) - headers <- content(req)$headers - names(headers) <- tolower(names(headers)) - headers - } - - expect_equal(round_trip(a = 1)$a, "1") - expect_equal(round_trip(a = "a + b")$a, "a + b") - +test_that("c.request merges headers", { + expect_equal(c(request(headers = c("a" = "a")), request(headers = c("b" = "b"))), + request(headers = c("a" = "a", "b" = "b"))) }) diff --git a/tests/testthat/test-response.r b/tests/testthat/test-response.r index 4be47ada..1c0490ff 100644 --- a/tests/testthat/test-response.r +++ b/tests/testthat/test-response.r @@ -1,5 +1,41 @@ context("Response") + +test_that("status codes returned as expected", { + + expect_equal(GET("http://httpbin.org/status/320")$status_code, 320) + expect_equal(GET("http://httpbin.org/status/404")$status_code, 404) + expect_equal(GET("http://httpbin.org/status/418")$status_code, 418) + +}) + +test_that("status converted to errors", { + + s200 <- GET("http://httpbin.org/status/200") + s300 <- GET("http://httpbin.org/status/300") + s404 <- GET("http://httpbin.org/status/404") + s500 <- GET("http://httpbin.org/status/500") + + expect_equal(stop_for_status(s200), TRUE) + expect_error(stop_for_status(s300), c("redirection.*\\(300\\)")) + expect_error(stop_for_status(s404), c("client.*\\(404\\)")) + expect_error(stop_for_status(s500), c("server.*\\(500\\)")) +}) + +test_that("headers returned as expected", { + round_trip <- function(...) { + req <- GET("http://httpbin.org/headers", add_headers(...)) + headers <- content(req)$headers + names(headers) <- tolower(names(headers)) + headers + } + + expect_equal(round_trip(a = 1)$a, "1") + expect_equal(round_trip(a = "a + b")$a, "a + b") + +}) + + test_that("application/json responses parsed as lists", { test_user_agent <- function(user_agent = NULL) { response <- GET("http://httpbin.org/user-agent",