summaryrefslogtreecommitdiffstats
path: root/R/http.R
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:42:37 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:42:37 -0300
commit11d49bc25eb1643f1b742f9527212d8781f2e0de (patch)
treebae1cb090cbf6c9f82ab5abbaf1194206f92f539 /R/http.R
downloadr-11d49bc25eb1643f1b742f9527212d8781f2e0de.tar.gz
Initial importv0.1.0
Diffstat (limited to 'R/http.R')
-rw-r--r--R/http.R210
1 files changed, 210 insertions, 0 deletions
diff --git a/R/http.R b/R/http.R
new file mode 100644
index 0000000..6ef4511
--- /dev/null
+++ b/R/http.R
@@ -0,0 +1,210 @@
+elapsed <- function() {
+ proc.time()[["elapsed"]]
+}
+
+MONTH_NUMBERS <- c(
+ Jan = 1L, Feb = 2L, Mar = 3L, Apr = 4L, May = 5L, Jun = 6L,
+ Jul = 7L, Aug = 8L, Sep = 9L, Oct = 10L, Nov = 11L, Dec = 12L
+)
+
+http_date_seconds <- function(value) {
+ parts <- regmatches(
+ value,
+ regexec(
+ paste0(
+ "^[A-Za-z]{3}, ([0-9]{2}) ([A-Za-z]{3}) ([0-9]{4}) ",
+ "([0-9]{2}):([0-9]{2}):([0-9]{2}) GMT$"
+ ),
+ value
+ )
+ )[[1L]]
+ if (length(parts) != 7L) {
+ return(NULL)
+ }
+ month <- MONTH_NUMBERS[parts[3L]]
+ if (is.na(month)) {
+ return(NULL)
+ }
+ target <- ISOdatetime(
+ as.integer(parts[4L]), month, as.integer(parts[2L]),
+ as.integer(parts[5L]), as.integer(parts[6L]), as.integer(parts[7L]),
+ tz = "GMT"
+ )
+ if (is.na(target)) {
+ return(NULL)
+ }
+ max(0, as.numeric(difftime(target, Sys.time(), units = "secs")))
+}
+
+retry_after_seconds <- function(value) {
+ if (is.null(value) || length(value) != 1L || is.na(value)) {
+ return(NULL)
+ }
+ value <- trimws(value)
+ if (!nzchar(value)) {
+ return(NULL)
+ }
+ seconds <- suppressWarnings(as.numeric(value))
+ if (!is.na(seconds)) {
+ return(max(0, seconds))
+ }
+ http_date_seconds(value)
+}
+
+backoff_wait <- function(client, attempt, retry_after) {
+ if (!is.null(retry_after)) {
+ return(min(retry_after, client$backoff_max))
+ }
+ ceiling <- min(client$backoff_base * 2^(attempt - 1L), client$backoff_max)
+ stats::runif(1L, 0, ceiling)
+}
+
+cepis_handle <- function(client) {
+ handle <- curl::new_handle()
+ curl::handle_setheaders(
+ handle,
+ "User-Agent" = client$user_agent,
+ "Accept" = "application/json, text/csv;q=0.9, */*;q=0.1",
+ "Accept-Encoding" = "identity"
+ )
+ curl::handle_setopt(
+ handle,
+ connecttimeout_ms = as.integer(client$timeout * 1000),
+ timeout_ms = as.integer(client$timeout * 1000)
+ )
+ handle
+}
+
+cepis_fetch <- function(client, path, accepted_status = 200L) {
+ url <- paste0(client$base_url, path)
+ deadline <- if (is.null(client$total_timeout)) {
+ NULL
+ } else {
+ elapsed() + client$total_timeout
+ }
+
+ last_status <- NULL
+ last_retry_after <- NULL
+ last_message <- NULL
+
+ for (attempt in seq_len(client$max_attempts)) {
+ if (!is.null(deadline) && elapsed() >= deadline) {
+ break
+ }
+
+ response <- tryCatch(
+ curl::curl_fetch_memory(url, handle = cepis_handle(client)),
+ error = function(e) e
+ )
+
+ if (inherits(response, "condition")) {
+ last_status <- NULL
+ last_retry_after <- NULL
+ last_message <- conditionMessage(response)
+ } else {
+ status <- as.integer(response$status_code)
+ headers <- curl::parse_headers_list(response$headers)
+ if (status %in% accepted_status || !(status %in% RETRIABLE_STATUS)) {
+ return(list(status = status, body = response$content,
+ headers = headers))
+ }
+ last_status <- status
+ last_retry_after <- retry_after_seconds(headers[["retry-after"]])
+ last_message <- NULL
+ }
+
+ if (attempt == client$max_attempts) {
+ break
+ }
+
+ wait <- backoff_wait(client, attempt, last_retry_after)
+ if (!is.null(deadline)) {
+ remaining <- deadline - elapsed()
+ if (remaining <= 0) {
+ break
+ }
+ wait <- min(wait, remaining)
+ }
+ Sys.sleep(wait)
+ }
+
+ attempts <- paste0(" (after ", client$max_attempts, " attempt(s))")
+ if (identical(last_status, 429L)) {
+ stop(rate_limited_error(
+ paste0("rate limit reached at ", url, attempts),
+ retry_after = last_retry_after
+ ))
+ }
+ if (!is.null(last_status)) {
+ stop(server_error(
+ paste0("the API answered ", last_status, " at ", url, attempts),
+ status = last_status
+ ))
+ }
+ stop(network_error(
+ paste0("could not reach ", url, attempts, ": ", last_message),
+ url = url
+ ))
+}
+
+decode_body <- function(body) {
+ if (length(body) >= 3L &&
+ identical(as.integer(body[1:3]), c(239L, 187L, 191L))) {
+ body <- body[-(1:3)]
+ }
+ text <- rawToChar(body)
+ Encoding(text) <- "UTF-8"
+ text
+}
+
+json_object <- function(body) {
+ text <- decode_body(body)
+ data <- tryCatch(
+ jsonlite::parse_json(text, simplifyVector = FALSE),
+ error = function(e) e
+ )
+ if (inherits(data, "condition") || !is.list(data) ||
+ (length(data) > 0L && is.null(names(data)))) {
+ stop(invalid_response_error(paste0(
+ "response is not a JSON object - something between you and the API is ",
+ "answering instead of it: ", substr(text, 1L, 120L)
+ )))
+ }
+ data
+}
+
+api_message <- function(body, fallback) {
+ data <- tryCatch(
+ jsonlite::parse_json(decode_body(body), simplifyVector = FALSE),
+ error = function(e) NULL
+ )
+ message <- if (is.list(data)) data[["mensagem"]] else NULL
+ if (is.character(message) && length(message) == 1L && nzchar(message)) {
+ message
+ } else {
+ fallback
+ }
+}
+
+csv_rows <- function(body) {
+ path <- tempfile(fileext = ".csv")
+ on.exit(unlink(path), add = TRUE)
+ writeBin(body, path)
+
+ table <- tryCatch(
+ utils::read.csv(
+ path,
+ colClasses = "character",
+ na.strings = character(0L),
+ check.names = FALSE,
+ encoding = "UTF-8"
+ ),
+ error = function(e) e
+ )
+ if (inherits(table, "condition")) {
+ stop(invalid_response_error(paste0(
+ "batch response is not valid CSV: ", conditionMessage(table)
+ )))
+ }
+ table
+}