DEFAULT_BASE_URL <- "https://cep.is" MAX_BATCH <- 500L RETRIABLE_STATUS <- c(429L, 500L, 502L, 503L, 504L) #' Build a client for the cep.is API #' #' @description #' Holds the configuration and nothing else: no connection, no state between #' calls. Build one and reuse it, or skip it entirely — [cep_lookup()], #' [cep_lookup_many()] and [cep_health()] fall back to a shared default #' client built with these defaults. #' #' @param base_url Where the API lives. Point it at a local instance (the API #' repo brings one up on `http://localhost:8080` with `just up`) or at a #' test server. It is the only parameter that usually changes between #' environments. Trailing slashes are stripped, so it cannot produce #' doubled separators that some proxies normalise and others 404. #' @param timeout Seconds allowed for **one attempt**, connection and #' transfer together. It is not a cap on the whole call, since a call may #' make several attempts; that cap is `total_timeout`. #' @param max_attempts How many attempts in total, counting the first. `1` #' disables retrying. The default of `3` covers the common case (one #' transient failure) without turning a real outage into a long wait. #' @param backoff_base Base of the exponential wait, in seconds. The actual #' wait is drawn from `[0, base * 2^(n - 1)]` — *full jitter*. The #' randomness is not decoration: without it, N clients that fail together #' come back together and reproduce the spike that took the service down. #' @param backoff_max Cap on each individual wait, in seconds. It applies to #' a server-sent `Retry-After` too, or a `Retry-After: 3600` from a #' misconfigured proxy would park the session for an hour. #' @param total_timeout Cap on the whole call, attempts and waits included. #' `NULL` leaves `max_attempts` as the only bound. Set it in code serving a #' web request: without it the worst case is `max_attempts * timeout` plus #' the sum of the backoffs. #' @param user_agent Overrides the `User-Agent`. The default identifies the #' package and its version, which is what lets the server diagnose an old #' client misbehaving. #' #' @return An object of class `cepis_client`, to pass as the `client` #' argument of [cep_lookup()], [cep_lookup_many()] and [cep_health()]. #' #' @examples #' client <- cepis_client(base_url = "http://localhost:8080", timeout = 5) #' client$max_attempts #' #' @export cepis_client <- function(base_url = DEFAULT_BASE_URL, timeout = 10, max_attempts = 3L, backoff_base = 0.5, backoff_max = 8, total_timeout = NULL, user_agent = NULL) { if (!is.character(base_url) || length(base_url) != 1L || is.na(base_url)) { stop("base_url must be a length-one character vector") } if (!is.numeric(timeout) || length(timeout) != 1L || is.na(timeout) || timeout <= 0) { stop("timeout must be a positive number of seconds") } if (!is.numeric(max_attempts) || length(max_attempts) != 1L || is.na(max_attempts) || max_attempts < 1) { stop("max_attempts must be >= 1") } if (!is.numeric(backoff_base) || length(backoff_base) != 1L || is.na(backoff_base) || backoff_base < 0) { stop("backoff_base must be >= 0") } if (!is.numeric(backoff_max) || length(backoff_max) != 1L || is.na(backoff_max) || backoff_max < 0) { stop("backoff_max must be >= 0") } if (!is.null(total_timeout) && (!is.numeric(total_timeout) || length(total_timeout) != 1L || is.na(total_timeout) || total_timeout <= 0)) { stop("total_timeout must be NULL or a positive number of seconds") } if (is.null(user_agent)) { user_agent <- paste0( "cepis-r/", as.character(utils::packageVersion("cepis")), " (+https://cep.is)" ) } structure( list( base_url = sub("/+$", "", base_url), timeout = as.numeric(timeout), max_attempts = as.integer(max_attempts), backoff_base = as.numeric(backoff_base), backoff_max = as.numeric(backoff_max), total_timeout = total_timeout, user_agent = user_agent ), class = "cepis_client" ) } #' @export print.cepis_client <- function(x, ...) { cat(" ", x$base_url, "\n", sep = "") cat(" timeout: ", x$timeout, "s per attempt, ", x$max_attempts, " attempt(s)\n", sep = "") cat(" backoff: ", x$backoff_base, "s base, ", x$backoff_max, "s cap\n", sep = "") cat(" total timeout: ", if (is.null(x$total_timeout)) "none" else paste0(x$total_timeout, "s"), "\n", sep = "") cat(" user agent: ", x$user_agent, "\n", sep = "") invisible(x) } cepis_state <- new.env(parent = emptyenv()) default_client <- function() { if (is.null(cepis_state$client)) { cepis_state$client <- cepis_client() } cepis_state$client }