diff --git a/NEWS.md b/NEWS.md index 54f42cc65..81627dba1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -50,7 +50,10 @@ is now deprecated (#314). * `req_perform_parallel()` replaces `multi_req_perform()`. `multi_req_perform()` - is now deprecated (#314). + is now deprecated (#314). `req_perform_parallel()` has a new error handling + strategy matching `req_perform_iterative()` and `req_perform_sequential()`. + It will bubble up errors by default but you can choose an alternative strategy + with the `on_error` argument. * `oauth_flow_auth_code()` allows the user to enter a URL that contains authorization `code` and `state` parameters (@fh-mthomson, #326). diff --git a/R/iterate-responses.R b/R/iterate-responses.R index 6bd6b2ec9..e95c2fa76 100644 --- a/R/iterate-responses.R +++ b/R/iterate-responses.R @@ -2,8 +2,8 @@ #' #' @description #' These function provide a basic toolkit for operating with lists of -#' responses as returned [req_perform_parallel()] and -#' [req_perform_iterative()]. +#' responses and possibly errors, as returned by [req_perform_parallel()], +#' [req_perform_sequential()] and [req_perform_iterative()]. #' #' * `resps_successes()` returns a list successful responses. #' * `resps_failures()` returns a list failed responses (i.e. errors). @@ -23,7 +23,7 @@ #' request(example_url()) |> req_template("/status/:status", status = 404), #' request("INVALID") #' ) -#' resps <- req_perform_parallel(reqs) +#' resps <- req_perform_parallel(reqs, on_error = "continue") #' #' # find successful responses #' resps |> resps_successes() diff --git a/R/iterate.R b/R/iterate.R index 68fe34a06..d3a59afc3 100644 --- a/R/iterate.R +++ b/R/iterate.R @@ -71,6 +71,11 @@ #' the iteration should terminate. #' @param max_reqs The maximum number of requests to perform. Use `Inf` to #' perform all requests until `next_req()` returns `NULL`. +#' @param on_error What should happen if a request fails? +#' +#' * `"stop"`, the default: stop iterating with an error. +#' * `"return"`: stop iterating, returning all the successful responses so +#' far, as well as an error object for the failed request. #' @param progress Display a progress bar? Use `TRUE` to turn on a basic #' progress bar, use a string to give it a name, or see [progress_bars] to #' customise it in other ways. @@ -78,7 +83,12 @@ #' a glue string that uses `{i}` to distinguish different requests. #' Useful for large responses because it avoids storing the response in #' memory. -#' @return A list of [response()]s. +#' @return +#' A list, at most length `max_reqs`, containing [response]s and possibly one +#' error object, if `on_error` is `"return"` and one of the requests errors. +#' If present, the error object will always be the last element in the list. +#' +#' Only httr2 errors are captured; see [req_error()] for more details. #' @export #' @examples #' req <- request(example_url()) |> @@ -99,14 +109,16 @@ #' ) #' }) req_perform_iterative <- function(req, - next_req, - path = NULL, - max_reqs = 20, - progress = TRUE) { + next_req, + path = NULL, + max_reqs = 20, + on_error = c("stop", "return"), + progress = TRUE) { check_request(req) check_function2(next_req, args = c("resp", "req")) check_number_whole(max_reqs, allow_infinite = TRUE, min = 1) check_string(path, allow_empty = FALSE, allow_null = TRUE) + on_error <- arg_match(on_error) get_path <- function(i) { if (is.null(path)) { @@ -127,7 +139,20 @@ req_perform_iterative <- function(req, tryCatch({ repeat { - resps[[i]] <- resp <- req_perform(req, path = get_path(i)) + httr2_error <- switch(on_error, + stop = function(cnd) zap(), + return = function(cnd) cnd + ) + resp <- try_fetch( + req_perform(req, path = get_path(i)), + httr2_error = httr2_error + ) + resps[[i]] <- resp + + if (on_error == "return" && is_error(resp)) { + break + } + progress$update() withCallingHandlers( diff --git a/R/multi-req.R b/R/multi-req.R index fcb88368a..180c18a10 100644 --- a/R/multi-req.R +++ b/R/multi-req.R @@ -2,25 +2,19 @@ #' #' @description #' This variation on [req_perform()] performs multiple requests in parallel. -#' Unlike `req_perform()` it always succeeds; it will never throw an error. -#' Instead it will return error objects, which are your responsibility to -#' handle. -#' #' Exercise caution when using this function; it's easy to pummel a server #' with many simultaneous requests. Only use it with hosts designed to serve -#' many files at once. +#' many files at once, which are typically web servers, not API servers. #' -#' # Limitations +#' `req_perform_parallel()` has a few limitations: #' #' * Will not retrieve a new OAuth token if it expires part way through #' the requests. #' * Does not perform throttling with [req_throttle()]. #' * Does not attempt retries as described by [req_retry()]. -#' * Consults the cache set by [req_cache()] before/after all requests. +#' * Only consults the cache set by [req_cache()] before/after all requests. #' -#' In general, where [req_perform()] might make multiple requests due to retries -#' or OAuth failures, `req_perform_parallel()` will only make 1. If any of -#' these limitations are problematic, you may want to use +#' If any of these limitations are problematic for your use case, we recommend #' [req_perform_sequential()] instead. #' #' @param reqs A list of [request]s. @@ -29,12 +23,21 @@ #' @param pool Optionally, a curl pool made by [curl::new_pool()]. Supply #' this if you want to override the defaults for total concurrent connections #' (100) or concurrent connections per host (6). -#' @param cancel_on_error Should all pending requests be cancelled when you -#' hit an error. Set this to `TRUE` to stop all requests as soon as you -#' hit an error. Responses that were never performed will have class -#' `httr2_cancelled` in the result. -#' @returns A list the same length as `reqs` where each element is either a -#' [response] or an `error`. +#' @param on_error What should happen if one of the requests fails? +#' +#' * `stop`, the default: stop iterating with an error. +#' * `return`: stop iterating, returning all the successful responses +#' received so far, as well as an error object for the failed request. +#' * `continue`: continue iterating, recording errors in the result. +#' @return +#' A list, the same length as `reqs`, containing [response]s and possibly +#' error objects, if `on_error` is `"return"` or `"continue"` and one of the +#' responses errors. If `on_error` is `"return"` and it errors on the ith +#' request, the ith element of the result will be an error object, and the +#' remaining elements will be `NULL`. If `on_error` is `"continue"`, it will +#' be a mix of requests and error objects. +#' +#' Only httr2 errors are captured; see [req_error()] for more details. #' @export #' @examples #' # Requesting these 4 pages one at a time would take 2 seconds: @@ -48,74 +51,103 @@ #' # But it's much faster if you request in parallel #' system.time(resps <- req_perform_parallel(reqs)) #' +#' # req_perform_parallel() will fail on error #' reqs <- list( #' request_base |> req_url_path("/status/200"), #' request_base |> req_url_path("/status/400"), #' request("FAILURE") #' ) -#' # req_perform_parallel() will always succeed -#' resps <- req_perform_parallel(reqs) +#' try(resps <- req_perform_parallel(reqs)) +#' +#' # but can use on_error to capture all successful results +#' resps <- req_perform_parallel(reqs, on_error = "continue") #' #' # Inspect the successful responses #' resps |> resps_successes() #' #' # And the failed responses #' resps |> resps_failures() |> resps_requests() -req_perform_parallel <- function(reqs, paths = NULL, pool = NULL, cancel_on_error = FALSE) { - if (!is.null(paths)) { - if (length(reqs) != length(paths)) { - cli::cli_abort("If supplied, {.arg paths} must be the same length as {.arg req}.") - } - } +req_perform_parallel <- function(reqs, + paths = NULL, + pool = NULL, + on_error = c("stop", "return", "continue")) { + check_paths(paths, reqs) + on_error <- arg_match(on_error) perfs <- vector("list", length(reqs)) for (i in seq_along(reqs)) { - perfs[[i]] <- Performance$new(req = reqs[[i]], path = paths[[i]]) + perfs[[i]] <- Performance$new( + req = reqs[[i]], + path = paths[[i]], + error_call = environment() + ) perfs[[i]]$submit(pool) } - pool_run(pool, perfs, cancel_on_error = cancel_on_error) + pool_run(pool, perfs, on_error = on_error) map(perfs, ~ .$resp) } + +#' Perform a list of requests in parallel +#' +#' @description +#' `r lifecycle::badge("deprecated")` +#' +#' Please use [req_perform_parallel()] instead, and note: +#' +#' * `cancel_on_error = FALSE` is now `on_error = "continue"` +#' * `cancel_on_error = TRUE` is now `on_error = "return"` +#' #' @export -#' @rdname req_perform_parallel -#' @usage NULL -multi_req_perform <- function(reqs, paths = NULL, pool = NULL, cancel_on_error = FALSE) { +#' @param cancel_on_error Should all pending requests be cancelled when you +#' hit an error? Set this to `TRUE` to stop all requests as soon as you +#' hit an error. Responses that were never performed be `NULL` in the result. +#' @inheritParams req_perform_parallel +#' @keywords internal +multi_req_perform <- function(reqs, + paths = NULL, + pool = NULL, + cancel_on_error = FALSE) { lifecycle::deprecate_warn( "1.0.0", "multi_req_perform()", "req_perform_parallel()" ) + check_bool(cancel_on_error) req_perform_parallel( reqs = reqs, paths = paths, pool = pool, - cancel_on_error = cancel_on_error + on_error = if (cancel_on_error) "continue" else "return" ) } -pool_run <- function(pool, perfs, cancel_on_error = FALSE) { - poll_until_done <- function(pool) { +pool_run <- function(pool, perfs, on_error = "continue") { + on.exit(pool_cancel(pool, perfs), add = TRUE) + + # The done and fail callbacks for curl::multi_add() are designed to always + # succeed. If the request actually failed, they raise a `httr_fail` + # signal (not error) that wraps the error. Here we catch that error and + # handle it based on the value of `on_error` + httr2_fail <- switch(on_error, + stop = function(cnd) cnd_signal(cnd$error), + continue = function(cnd) zap(), + return = function(cnd) NULL + ) + + try_fetch( repeat({ # TODO: progress bar run <- curl::multi_run(0.1, pool = pool, poll = TRUE) if (run$pending == 0) { break } - }) - } - - cancel <- function(cnd) pool_cancel(pool, perfs) - if (!cancel_on_error) { - tryCatch(poll_until_done(pool), interrupt = cancel) - } else { - tryCatch(poll_until_done(pool), interrupt = cancel, `httr2:::failed` = cancel) - } - - # Ensuring any pending handles are still completed - curl::multi_run(pool = pool) + }), + interrupt = function(cnd) NULL, + httr2_fail = httr2_fail + ) invisible() } @@ -128,10 +160,12 @@ Performance <- R6Class("Performance", public = list( handle = NULL, resp = NULL, pool = NULL, + error_call = NULL, - initialize = function(req, path = NULL) { + initialize = function(req, path = NULL, error_call = NULL) { self$req <- req self$path <- path + self$error_call <- error_call req <- auth_oauth_sign(req) req <- cache_pre_fetch(req) @@ -145,12 +179,13 @@ Performance <- R6Class("Performance", public = list( submit = function(pool = NULL) { if (!is.null(self$resp)) { + # cached return() } self$pool <- pool - self$resp <- error_cnd("httr2_cancelled", message = "Request cancelled") - curl::multi_add(self$handle, + curl::multi_add( + handle = self$handle, pool = self$pool, data = self$path, done = self$succeed, @@ -171,18 +206,27 @@ Performance <- R6Class("Performance", public = list( ) resp <- cache_post_fetch(self$reqs, resp, path = self$paths) - self$resp <- tryCatch(resp_check_status(resp), error = identity) + self$resp <- tryCatch( + resp_check_status(resp, error_call = self$error_call), + error = identity + ) if (is_error(self$resp)) { - signal("", class = "httr2:::failed") + signal("", error = self$resp, class = "httr2_fail") } }, fail = function(msg) { - self$resp <- error_cnd("httr2_failure", message = msg, request = self$req) - signal("", class = "httr2:::failed") + self$resp <- error_cnd( + "httr2_failure", + message = msg, + request = self$req, + call = self$error_call + ) + signal("", error = self$resp, class = "httr2_fail") }, cancel = function() { + # No handle if response was cached if (!is.null(self$handle)) { curl::multi_cancel(self$handle) } @@ -191,4 +235,5 @@ Performance <- R6Class("Performance", public = list( pool_cancel <- function(pool, perfs) { walk(perfs, ~ .x$cancel()) + curl::multi_run(pool = pool) } diff --git a/R/sequential.R b/R/sequential.R index d35a186a3..40fcdb360 100644 --- a/R/sequential.R +++ b/R/sequential.R @@ -6,21 +6,8 @@ #' #' @inheritParams req_perform_parallel #' @inheritParams req_perform_iterative -#' @param on_error What should happen if one of the requests throws an -#' HTTP error? -#' -#' * `stop`, the default: stop iterating and throw an error -#' * `return`: stop iterating and return all the successful responses so far. -#' * `continue`: continue iterating, recording errors in the result. +#' @inherit req_perform_parallel return #' @export -#' @return -#' A list, the same length as `reqs`. -#' -#' If `on_error` is `"return"` and it errors on the ith request, the ith -#' element of the result will be an error object, and the remaining elements -#' will be `NULL`. -#' -#' If `on_error` is `"continue"`, it will be a mix of requests and errors. #' @examples #' # One use of req_perform_sequential() is if the API allows you to request #' # data for multiple objects, you want data for more objects than can fit @@ -49,13 +36,9 @@ req_perform_sequential <- function(reqs, if (!is_bare_list(reqs)) { stop_input_type(reqs, "a list") } - if (!is.null(paths)) { - check_character(paths) - if (length(reqs) != length(paths)) { - cli::cli_abort("If supplied, {.arg paths} must be the same length as {.arg req}.") - } - } + check_paths(paths, reqs) on_error <- arg_match(on_error) + err_catch <- on_error != "stop" err_return <- on_error == "return" @@ -92,3 +75,15 @@ req_perform_sequential <- function(reqs, resps } + +check_paths <- function(paths, reqs, error_call = caller_env()) { + if (!is.null(paths)) { + check_character(paths) + if (length(reqs) != length(paths)) { + cli::cli_abort( + "If supplied, {.arg paths} must be the same length as {.arg req}.", + call = error_call + ) + } + } +} diff --git a/man/multi_req_perform.Rd b/man/multi_req_perform.Rd new file mode 100644 index 000000000..27adc4f4c --- /dev/null +++ b/man/multi_req_perform.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/multi-req.R +\name{multi_req_perform} +\alias{multi_req_perform} +\title{Perform a list of requests in parallel} +\usage{ +multi_req_perform(reqs, paths = NULL, pool = NULL, cancel_on_error = FALSE) +} +\arguments{ +\item{reqs}{A list of \link{request}s.} + +\item{paths}{An optional list of paths, if you want to download the request +bodies to disks. If supplied, must be the same length as \code{reqs}.} + +\item{pool}{Optionally, a curl pool made by \code{\link[curl:multi]{curl::new_pool()}}. Supply +this if you want to override the defaults for total concurrent connections +(100) or concurrent connections per host (6).} + +\item{cancel_on_error}{Should all pending requests be cancelled when you +hit an error? Set this to \code{TRUE} to stop all requests as soon as you +hit an error. Responses that were never performed be \code{NULL} in the result.} +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} + +Please use \code{\link[=req_perform_parallel]{req_perform_parallel()}} instead, and note: +\itemize{ +\item \code{cancel_on_error = FALSE} is now \code{on_error = "continue"} +\item \code{cancel_on_error = TRUE} is now \code{on_error = "return"} +} +} +\keyword{internal} diff --git a/man/req_perform_iterative.Rd b/man/req_perform_iterative.Rd index cd1e44d92..d3dd93c06 100644 --- a/man/req_perform_iterative.Rd +++ b/man/req_perform_iterative.Rd @@ -9,6 +9,7 @@ req_perform_iterative( next_req, path = NULL, max_reqs = 20, + on_error = c("stop", "return"), progress = TRUE ) } @@ -27,12 +28,23 @@ memory.} \item{max_reqs}{The maximum number of requests to perform. Use \code{Inf} to perform all requests until \code{next_req()} returns \code{NULL}.} +\item{on_error}{What should happen if a request fails? +\itemize{ +\item \code{"stop"}, the default: stop iterating with an error. +\item \code{"return"}: stop iterating, returning all the successful responses so +far, as well as an error object for the failed request. +}} + \item{progress}{Display a progress bar? Use \code{TRUE} to turn on a basic progress bar, use a string to give it a name, or see \link{progress_bars} to customise it in other ways.} } \value{ -A list of \code{\link[=response]{response()}}s. +A list, at most length \code{max_reqs}, containing \link{response}s and possibly one +error object, if \code{on_error} is \code{"return"} and one of the requests errors. +If present, the error object will always be the last element in the list. + +Only httr2 errors are captured; see \code{\link[=req_error]{req_error()}} for more details. } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} diff --git a/man/req_perform_parallel.Rd b/man/req_perform_parallel.Rd index fea6bef32..5468f0b35 100644 --- a/man/req_perform_parallel.Rd +++ b/man/req_perform_parallel.Rd @@ -2,10 +2,14 @@ % Please edit documentation in R/multi-req.R \name{req_perform_parallel} \alias{req_perform_parallel} -\alias{multi_req_perform} \title{Perform a list of requests in parallel} \usage{ -req_perform_parallel(reqs, paths = NULL, pool = NULL, cancel_on_error = FALSE) +req_perform_parallel( + reqs, + paths = NULL, + pool = NULL, + on_error = c("stop", "return", "continue") +) } \arguments{ \item{reqs}{A list of \link{request}s.} @@ -17,40 +21,42 @@ bodies to disks. If supplied, must be the same length as \code{reqs}.} this if you want to override the defaults for total concurrent connections (100) or concurrent connections per host (6).} -\item{cancel_on_error}{Should all pending requests be cancelled when you -hit an error. Set this to \code{TRUE} to stop all requests as soon as you -hit an error. Responses that were never performed will have class -\code{httr2_cancelled} in the result.} +\item{on_error}{What should happen if one of the requests fails? +\itemize{ +\item \code{stop}, the default: stop iterating with an error. +\item \code{return}: stop iterating, returning all the successful responses +received so far, as well as an error object for the failed request. +\item \code{continue}: continue iterating, recording errors in the result. +}} } \value{ -A list the same length as \code{reqs} where each element is either a -\link{response} or an \code{error}. +A list, the same length as \code{reqs}, containing \link{response}s and possibly +error objects, if \code{on_error} is \code{"return"} or \code{"continue"} and one of the +responses errors. If \code{on_error} is \code{"return"} and it errors on the ith +request, the ith element of the result will be an error object, and the +remaining elements will be \code{NULL}. If \code{on_error} is \code{"continue"}, it will +be a mix of requests and error objects. + +Only httr2 errors are captured; see \code{\link[=req_error]{req_error()}} for more details. } \description{ This variation on \code{\link[=req_perform]{req_perform()}} performs multiple requests in parallel. -Unlike \code{req_perform()} it always succeeds; it will never throw an error. -Instead it will return error objects, which are your responsibility to -handle. - Exercise caution when using this function; it's easy to pummel a server with many simultaneous requests. Only use it with hosts designed to serve -many files at once. -} -\section{Limitations}{ +many files at once, which are typically web servers, not API servers. + +\code{req_perform_parallel()} has a few limitations: \itemize{ \item Will not retrieve a new OAuth token if it expires part way through the requests. \item Does not perform throttling with \code{\link[=req_throttle]{req_throttle()}}. \item Does not attempt retries as described by \code{\link[=req_retry]{req_retry()}}. -\item Consults the cache set by \code{\link[=req_cache]{req_cache()}} before/after all requests. +\item Only consults the cache set by \code{\link[=req_cache]{req_cache()}} before/after all requests. } -In general, where \code{\link[=req_perform]{req_perform()}} might make multiple requests due to retries -or OAuth failures, \code{req_perform_parallel()} will only make 1. If any of -these limitations are problematic, you may want to use +If any of these limitations are problematic for your use case, we recommend \code{\link[=req_perform_sequential]{req_perform_sequential()}} instead. } - \examples{ # Requesting these 4 pages one at a time would take 2 seconds: request_base <- request(example_url()) @@ -63,13 +69,16 @@ reqs <- list( # But it's much faster if you request in parallel system.time(resps <- req_perform_parallel(reqs)) +# req_perform_parallel() will fail on error reqs <- list( request_base |> req_url_path("/status/200"), request_base |> req_url_path("/status/400"), request("FAILURE") ) -# req_perform_parallel() will always succeed -resps <- req_perform_parallel(reqs) +try(resps <- req_perform_parallel(reqs)) + +# but can use on_error to capture all successful results +resps <- req_perform_parallel(reqs, on_error = "continue") # Inspect the successful responses resps |> resps_successes() diff --git a/man/req_perform_sequential.Rd b/man/req_perform_sequential.Rd index fb4c502aa..6a3b0547d 100644 --- a/man/req_perform_sequential.Rd +++ b/man/req_perform_sequential.Rd @@ -17,11 +17,11 @@ req_perform_sequential( \item{paths}{An optional list of paths, if you want to download the request bodies to disks. If supplied, must be the same length as \code{reqs}.} -\item{on_error}{What should happen if one of the requests throws an -HTTP error? +\item{on_error}{What should happen if one of the requests fails? \itemize{ -\item \code{stop}, the default: stop iterating and throw an error -\item \code{return}: stop iterating and return all the successful responses so far. +\item \code{stop}, the default: stop iterating with an error. +\item \code{return}: stop iterating, returning all the successful responses +received so far, as well as an error object for the failed request. \item \code{continue}: continue iterating, recording errors in the result. }} @@ -30,13 +30,14 @@ progress bar, use a string to give it a name, or see \link{progress_bars} to customise it in other ways.} } \value{ -A list, the same length as \code{reqs}. +A list, the same length as \code{reqs}, containing \link{response}s and possibly +error objects, if \code{on_error} is \code{"return"} or \code{"continue"} and one of the +responses errors. If \code{on_error} is \code{"return"} and it errors on the ith +request, the ith element of the result will be an error object, and the +remaining elements will be \code{NULL}. If \code{on_error} is \code{"continue"}, it will +be a mix of requests and error objects. -If \code{on_error} is \code{"return"} and it errors on the ith request, the ith -element of the result will be an error object, and the remaining elements -will be \code{NULL}. - -If \code{on_error} is \code{"continue"}, it will be a mix of requests and errors. +Only httr2 errors are captured; see \code{\link[=req_error]{req_error()}} for more details. } \description{ Given a list of requests, this function performs each in turn, returning diff --git a/man/resps_successes.Rd b/man/resps_successes.Rd index e6dc7d9fc..5fe3cc5fa 100644 --- a/man/resps_successes.Rd +++ b/man/resps_successes.Rd @@ -23,8 +23,8 @@ returns the data foind inside that response as a vector or data frame.} } \description{ These function provide a basic toolkit for operating with lists of -responses as returned \code{\link[=req_perform_parallel]{req_perform_parallel()}} and -\code{\link[=req_perform_iterative]{req_perform_iterative()}}. +responses and possibly errors, as returned by \code{\link[=req_perform_parallel]{req_perform_parallel()}}, +\code{\link[=req_perform_sequential]{req_perform_sequential()}} and \code{\link[=req_perform_iterative]{req_perform_iterative()}}. \itemize{ \item \code{resps_successes()} returns a list successful responses. \item \code{resps_failures()} returns a list failed responses (i.e. errors). @@ -41,7 +41,7 @@ reqs <- list( request(example_url()) |> req_template("/status/:status", status = 404), request("INVALID") ) -resps <- req_perform_parallel(reqs) +resps <- req_perform_parallel(reqs, on_error = "continue") # find successful responses resps |> resps_successes() diff --git a/tests/testthat/_snaps/multi-req.md b/tests/testthat/_snaps/multi-req.md index f28f882fe..88eb27b2b 100644 --- a/tests/testthat/_snaps/multi-req.md +++ b/tests/testthat/_snaps/multi-req.md @@ -6,6 +6,19 @@ Error in `req_perform_parallel()`: ! If supplied, `paths` must be the same length as `req`. +# errors by default + + Code + req_perform_parallel(reqs[1]) + Condition + Error in `req_perform_parallel()`: + ! HTTP 404 Not Found. + Code + req_perform_parallel(reqs[2]) + Condition + Error in `req_perform_parallel()`: + ! Could not resolve host: INVALID + # multi_req_perform is deprecated Code diff --git a/tests/testthat/test-iterate-responses.R b/tests/testthat/test-iterate-responses.R index 83bee93f3..d9ba95fdf 100644 --- a/tests/testthat/test-iterate-responses.R +++ b/tests/testthat/test-iterate-responses.R @@ -4,7 +4,7 @@ test_that("basic helpers work", { request_test("/status/:status", status = 404), request("INVALID") ) - resps <- req_perform_parallel(reqs) + resps <- req_perform_parallel(reqs, on_error = "continue") expect_equal(resps_successes(resps), resps[1]) expect_equal(resps_failures(resps), resps[2:3]) diff --git a/tests/testthat/test-iterate.R b/tests/testthat/test-iterate.R index f43a53951..4bbf55162 100644 --- a/tests/testthat/test-iterate.R +++ b/tests/testthat/test-iterate.R @@ -63,6 +63,21 @@ test_that("can retrieve all pages", { expect_length(resps, 120) }) +test_that("can choose to return on failure", { + iterator <- function(resp, req) { + request_test("/status/:status", status = 404) + } + expect_error( + req_perform_iterative(request_test(), iterator), + class = "httr2_http_404" + ) + + out <- req_perform_iterative(request_test(), iterator, on_error = "return") + expect_length(out, 2) + expect_s3_class(out[[1]], "httr2_response") + expect_s3_class(out[[2]], "httr2_http_404") +}) + test_that("checks its inputs", { req <- request_test() expect_snapshot(error = TRUE,{ diff --git a/tests/testthat/test-multi-req.R b/tests/testthat/test-multi-req.R index 343f666a4..e38dde8e8 100644 --- a/tests/testthat/test-multi-req.R +++ b/tests/testthat/test-multi-req.R @@ -49,12 +49,23 @@ test_that("immutable objects retrieved from cache", { expect_equal(resps[[1]], resp) }) -test_that("both curl and HTTP errors become errors", { +test_that("errors by default", { + reqs <- list2( + request_test("/status/:status", status = 404), + request("INVALID") + ) + expect_snapshot(error = TRUE, { + req_perform_parallel(reqs[1]) + req_perform_parallel(reqs[2]) + }) +}) + +test_that("both curl and HTTP errors become errors on continue", { reqs <- list2( request_test("/status/:status", status = 404), request("INVALID"), ) - out <- req_perform_parallel(reqs) + out <- req_perform_parallel(reqs, on_error = "continue") expect_s3_class(out[[1]], "httr2_http_404") expect_s3_class(out[[2]], "httr2_failure") @@ -68,17 +79,9 @@ test_that("errors can cancel outstanding requests", { request_test("/status/:status", status = 404), request_test("/delay/:secs", secs = 2), ) - out <- req_perform_parallel(reqs, cancel_on_error = TRUE) + out <- req_perform_parallel(reqs, on_error = "return") expect_s3_class(out[[1]], "httr2_http_404") - expect_s3_class(out[[2]], "httr2_cancelled") - - reqs <- list2( - request("blah://INVALID"), - request_test("/delay/:secs", secs = 2), - ) - out <- req_perform_parallel(reqs, cancel_on_error = TRUE) - expect_s3_class(out[[1]], "httr2_failure") - expect_s3_class(out[[2]], "httr2_cancelled") + expect_null(out[[2]]) }) test_that("multi_req_perform is deprecated", {