File: response.R

package info (click to toggle)
r-cran-crul 1.0.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 1,592 kB
  • sloc: sh: 13; makefile: 2
file content (308 lines) | stat: -rw-r--r-- 11,383 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#' @title Base HTTP response object
#' @description Class with methods for handling HTTP responses
#' 
#' @export
#' @seealso [content-types]
#' @details
#' **Additional Methods**
#'   \describe{
#'     \item{`raise_for_ct(type, charset = NULL, behavior = "stop")`}{
#'       Check response content-type; stop or warn if not matched. Parameters:
#'       \itemize{
#'        \item type: (character) a mime type to match against; see
#'          [mime::mimemap] for allowed values
#'        \item charset: (character) if a charset string given, we check that
#'          it matches the charset in the content type header. default: NULL
#'        \item behavior: (character) one of stop (default) or warning
#'       }
#'     }
#'     \item{`raise_for_ct_html(charset = NULL, behavior = "stop")`}{
#'       Check that the response content-type is `text/html`; stop or warn if
#'       not matched. Parameters: see `raise_for_ct()`
#'     }
#'     \item{`raise_for_ct_json(charset = NULL, behavior = "stop")`}{
#'       Check that the response content-type is `application/json`; stop or
#'       warn if not matched. Parameters: see `raise_for_ct()`
#'     }
#'     \item{`raise_for_ct_xml(charset = NULL, behavior = "stop")`}{
#'       Check that the response content-type is `application/xml`; stop or warn if
#'       not matched. Parameters: see `raise_for_ct()`
#'     }
#'   }
#' @examples \dontrun{
#' x <- HttpResponse$new(method = "get", url = "https://httpbin.org")
#' x$url
#' x$method
#'
#' x <- HttpClient$new(url = 'https://httpbin.org')
#' (res <- x$get('get'))
#' res$request_headers
#' res$response_headers
#' res$parse()
#' res$status_code
#' res$status_http()
#' res$status_http()$status_code
#' res$status_http()$message
#' res$status_http()$explanation
#' res$success()
#'
#' x <- HttpClient$new(url = 'https://httpbin.org/status/404')
#' (res <- x$get())
#' # res$raise_for_status()
#'
#' x <- HttpClient$new(url = 'https://httpbin.org/status/414')
#' (res <- x$get())
#' # res$raise_for_status()
#' }
HttpResponse <- R6::R6Class(
  "HttpResponse",
  public = list(
    #' @field method (character) one or more URLs
    method = NULL,
    #' @field url (character) one or more URLs
    url = NULL,
    #' @field opts (character) one or more URLs
    opts = NULL,
    #' @field handle (character) one or more URLs
    handle = NULL,
    #' @field status_code (character) one or more URLs
    status_code = NULL,
    #' @field request_headers (character) one or more URLs
    request_headers = NULL,
    #' @field response_headers (character) one or more URLs
    response_headers = NULL,
    #' @field response_headers_all (character) one or more URLs
    response_headers_all = NULL,
    #' @field modified (character) one or more URLs
    modified = NULL,
    #' @field times (character) one or more URLs
    times = NULL,
    #' @field content (character) one or more URLs
    content = NULL,
    #' @field request (character) one or more URLs
    request = NULL,
    #' @field raise_for_ct for ct method (general)
    raise_for_ct = NULL,
    #' @field raise_for_ct_html for ct method (html)
    raise_for_ct_html = NULL,
    #' @field raise_for_ct_json for ct method (json)
    raise_for_ct_json = NULL,
    #' @field raise_for_ct_xml for ct method (xml)
    raise_for_ct_xml = NULL,

    #' @description print method for HttpResponse objects
    #' @param x self
    #' @param ... ignored
    print = function(x, ...) {
      cat("<crul response> ", sep = "\n")
      cat(paste0("  url: ", self$url), sep = "\n")

      cat("  request_headers: ", sep = "\n")
      if (length(self$request_headers)) {
        for (i in seq_along(self$request_headers)) {
          cat(sprintf("    %s: %s", names(self$request_headers)[i],
                      self$request_headers[[i]]), sep = "\n")
        }
      }

      cat("  response_headers: ", sep = "\n")
      if (length(self$response_headers)) {
        for (i in seq_along(self$response_headers)) {
          cat(sprintf("    %s: %s", names(self$response_headers)[i],
                      self$response_headers[[i]]), sep = "\n")
        }
      }

      params <- parse_params(self$url)
      if (!is.null(params)) {
        cat("  params: ", sep = "\n")
        for (i in seq_along(params)) {
          cat(paste0("    ", sub("=", ": ", params[[i]], "=")), sep = "\n")
        }
      }
      if (!is.null(self$status_code)) cat(paste0("  status: ",
                                                 self$status_code), sep = "\n")
      invisible(self)
    },

    #' @description Create a new HttpResponse object
    #' @param method (character) HTTP method
    #' @param url (character) A url, required
    #' @param opts (list) curl options
    #' @param handle A handle
    #' @param status_code (integer) status code
    #' @param request_headers (list) request headers, named list
    #' @param response_headers (list) response headers, named list
    #' @param response_headers_all (list) all response headers, including
    #' intermediate redirect headers, unnamed list of named lists
    #' @param modified (character) modified date
    #' @param times (vector) named vector
    #' @param content (raw) raw binary content response
    #' @param request request object, with all details
    initialize = function(method, url, opts, handle, status_code,
                          request_headers, response_headers,
                          response_headers_all, modified, times,
                          content, request) {

      if (!missing(method)) self$method <- method
      self$url <- url
      if (!missing(opts)) self$opts <- opts
      if (!missing(handle)) self$handle <- handle
      if (!missing(status_code)) self$status_code <- as.numeric(status_code)
      if (!missing(request_headers)) self$request_headers <- request_headers
      if (!missing(response_headers)) self$response_headers <- response_headers
      if (!missing(response_headers_all))
        self$response_headers_all <- response_headers_all
      if (!missing(modified)) self$modified <- modified
      if (!missing(times)) self$times <- times
      if (!missing(content)) self$content <- content
      if (!missing(request)) self$request <- request

      self$raise_for_ct = private$raise_for_ct_user()
      self$raise_for_ct_html = private$raise_for_ct_factory(type = "html")
      self$raise_for_ct_json = private$raise_for_ct_factory(type = "json")
      self$raise_for_ct_xml = private$raise_for_ct_factory(type = "xml")
    },

    #' @description Parse the raw response content to text
    #' @param encoding (character) A character string describing the
    #' current encoding. If left as `NULL`, we attempt to guess the
    #' encoding. Passed to `from` parameter in `iconv`
    #' @param ... additional parameters passed on to `iconv` (options: sub,
    #' mark, toRaw). See `?iconv` for help
    #' @return character string
    parse = function(encoding = NULL, ...) {
      if (
        "disk" %in% names(self$request) ||
        (inherits(self$request, "HttpRequest") &&
          "disk" %in% names(self$request$payload))
      ) {
        if (
          inherits(self$request, "HttpRequest") &&
          length(self$content) == 0
        ) {
          pld <- self$request$payload$disk
        } else if (inherits(self$content, "raw")) {
          return(parse_content(self$content, encoding, ...))
        } else {
          pld <- self$content
        }
        raw <- readBin(pld, "raw", file.info(pld)$size)
        try_raw2ch <- tryCatch(rawToChar(raw), error = function(e) e)
        rawout <- if (inherits(try_raw2ch, "error")) raw else rawToChar(raw)
        return(rawout)
      }
      if ("stream" %in% names(self$request)) {
        return(raw(0))
      }
      parse_content(self$content, encoding, ...)
    },

    #' @description Was status code less than or equal to 201
    #' @return boolean
    success = function() {
      self$status_code < 400L && self$status_code >= 200L
    },

    #' @description Get HTTP status code, message, and explanation
    #' @param verbose (logical) whether to get verbose http status description,
    #' default: `FALSE`
    #' @return object of class "http_code", a list with slots for status_code,
    #' message, and explanation
    status_http = function(verbose = FALSE) {
      httpcode::http_code(code = self$status_code, verbose = verbose)
    },

    #' @description Check HTTP status and stop with appropriate
    #' HTTP error code and message if >= 300. otherwise use \pkg{httpcode}.
    #' If you have `fauxpas` installed we use that.
    #' @return stop or warn with message
    raise_for_status = function() {
      if (self$status_code >= 300) {
        if (!requireNamespace("fauxpas", quietly = TRUE)) {
          x <- httpcode::http_code(code = self$status_code)
          stop(sprintf("%s (HTTP %s)", x$message, x$status_code), call. = FALSE)
        } else {
          fauxpas::http(self, behavior = "stop")
        }
      }
    }
  ),

  private = list(
    raise_for_ct_user = function() {
      function(type, charset = NULL, behavior = "stop") {
        if (!type %in% mime::mimemap)
          stop("type not in allowed set, see ?mime::mimemap")
        type <- names(mime::mimemap[type == mime::mimemap])[1]
        private$raise_for_ct_factory(type)(
          charset = charset, behavior = behavior
        )
      }
    },
    raise_for_ct_factory = function(type) {
      function(charset = NULL, behavior = "stop") {
        behaviors <- c("stop", "warning")
        assert(behavior, "character")
        if (!behavior %in% behaviors)
          stop("'behavior' must be one of ", paste(behaviors, collapse = ", "))
        ctype <- mime::mimemap[[type]]
        if (is.null(self$response_headers$`content-type`))
          stop("content-type header is missing")
        rtype <- self$response_headers$`content-type`
        if (!is.null(charset)) {
          if (!grepl(";\\s?[A-Za-z0-9]+|;\\s?charset=[A-Za-z0-9]+", rtype)) {
            warning("no charset detected in response content-type",
              call. = FALSE)
          } else if (
            !grepl(ctype, rtype) ||
            !grepl(norm(charset), norm(rtype))
          ) {
            get(behavior)(sprintf("response content-type (%s) did not match expected type (%s)\nor character set (%s)", rtype, ctype, charset), call. = FALSE)
          }
        } else {
          if (!grepl(ctype, rtype)) {
            get(behavior)(sprintf("response content-type (%s) did not match expected type (%s)",
              rtype, ctype), call. = FALSE)
          }
        }
      }
    }
  )
)

# remove spaces; lowercase everything
norm <- function(x) {
  x <- gsub("\\s", "", x)
  x <- tolower(x)
  return(x)
}

guess_encoding <- function(encoding = NULL) {
  if (!is.null(encoding)) {
    return(check_encoding(encoding))
  } else {
    message("No encoding supplied: defaulting to UTF-8.")
    return("UTF-8")
  }
}

check_encoding <- function(x) {
  if ((tolower(x) %in% tolower(iconvlist()))) return(x)
  message("Invalid encoding ", x, ": defaulting to UTF-8.")
  "UTF-8"
}

parse_params <- function(x) {
  x <- urltools::parameters(x)
  if (is.na(x)) {
    NULL
  } else {
    strsplit(x, "&")[[1]]
  }
}

parse_content <- function(x, encoding, ...) {
  iconv(x = readBin(x, character()),
    from = guess_encoding(encoding), to = "UTF-8", ...)
}