docs: def DXHTTPRequest(resource, data, method='POST', headers=None, auth=True,
                    timeout=DEFAULT_TIMEOUT,
                    use_compression=None, jsonify_data=True, want_full_response=False,
                    decode_response_body=True, prepend_srv=True, session_handler=None,
                    max_retries=DEFAULT_RETRIES, always_retry=False,
                    **kwargs):
      '''
      :param resource: API server route, e.g. "/record/new". If *prepend_srv* is False, a fully qualified URL is expected. If this argument is a callable, it will be called just before each request attempt, and expected to return a tuple (URL, headers). Headers returned by the callback are updated with *headers* (including headers set by this method).
      :type resource: string
      :param data: Content of the request body
      :type data: list or dict, if *jsonify_data* is True; or string or file-like object, otherwise
      :param headers: Names and values of HTTP headers to submit with the request (in addition to those needed for authentication, compression, or other options specified with the call).
      :type headers: dict
      :param auth:
          Controls the ``Authentication`` header or other means of authentication supplied with the request. If ``True``
          (default), a token is obtained from the ``DX_SECURITY_CONTEXT``. If the value evaluates to false, no action is
          taken to prepare authentication for the request. Otherwise, the value is assumed to be callable, and called with
          three arguments (method, url, headers) and expected to prepare the authentication headers by reference.
      :type auth: tuple, object, True (default), or None
      :param timeout: HTTP request timeout, in seconds
      :type timeout: float
      :param config: *config* value to pass through to :meth:`requests.request`
      :type config: dict
      :param use_compression: Deprecated
      :type use_compression: string or None
      :param jsonify_data: If True, *data* is converted from a Python list or dict to a JSON string
      :type jsonify_data: boolean
      :param want_full_response: If True, the full :class:`requests.Response` object is returned (otherwise, only the content of the response body is returned)
      :type want_full_response: boolean
      :param decode_response_body: If True (and *want_full_response* is False), the response body is decoded and, if it is a JSON string, deserialized. Otherwise, the response body is uncompressed if transport compression is on, and returned raw.
      :type decode_response_body: boolean
      :param prepend_srv: If True, prepends the API server location to the URL
      :type prepend_srv: boolean
      :param session_handler: Deprecated.
      :param max_retries: Maximum number of retries to perform for a request. A "failed" request is retried if any of the following is true:
  
                          - A response is received from the server, and the content length received does not match the "Content-Length" header.
                          - A response is received from the server, and the response has an HTTP status code in 5xx range.
                          - A response is received from the server, the "Content-Length" header is not set, and the response JSON cannot be parsed.
                          - No response is received from the server, and either *always_retry* is True or the request *method* is "GET".
  
      :type max_retries: int
      :param always_retry: If True, indicates that it is safe to retry a request on failure
  
                          - Note: It is not guaranteed that the request will *always* be retried on failure; rather, this is an indication to the function that it would be safe to do so.
  
      :type always_retry: boolean
      :returns: Response from API server in the format indicated by *want_full_response* and *decode_response_body*.
      :raises: :exc:`exceptions.DXAPIError` or a subclass if the server returned a non-200 status code; :exc:`requests.exceptions.HTTPError` if an invalid response was received from the server; or :exc:`requests.exceptions.ConnectionError` if a connection cannot be established.
  
      Wrapper around :meth:`requests.request()` that makes an HTTP
      request, inserting authentication headers and (by default)
      converting *data* to JSON.
  
      .. note:: Bindings methods that make API calls make the underlying
         HTTP request(s) using :func:`DXHTTPRequest`, and most of them
         will pass any unrecognized keyword arguments you have supplied
         through to :func:`DXHTTPRequest`.
  
      '''
      if headers is None:
          headers = {}
  
      global _UPGRADE_NOTIFY
  
      seq_num = _get_sequence_number()
  
      url = APISERVER + resource if prepend_srv else resource
      method = method.upper()  # Convert method name to uppercase, to ease string comparisons later
  
      if auth is True:
          auth = AUTH_HELPER
  
      if auth:
          auth(_RequestForAuth(method, url, headers))
  
      pool_args = {arg: kwargs.pop(arg, None) for arg in ("verify", "cert_file", "key_file")}
      test_retry = kwargs.pop("_test_retry_http_request", False)
  
      # data is a sequence/buffer or a dict
      # serialized_data is a sequence/buffer
  
      if jsonify_data:
          serialized_data = json.dumps(data)
          if 'Content-Type' not in headers and method == 'POST':
              headers['Content-Type'] = 'application/json'
      else:
          serialized_data = data
  
      # If the input is a buffer, its data gets consumed by
      # requests.request (moving the read position). Record the initial
      # buffer position so that we can return to it if the request fails
      # and needs to be retried.
      rewind_input_buffer_offset = None
      if hasattr(data, 'seek') and hasattr(data, 'tell'):
          rewind_input_buffer_offset = data.tell()
  
      # Maintain two separate counters for the number of tries...
  
      try_index = 0  # excluding 503 errors. The number of tries as given here
                     # cannot exceed (max_retries + 1).
      try_index_including_503 = 0  # including 503 errors. This number is used to
                                   # do exponential backoff.
  
      retried_responses = []
      _url = None
      while True:
          success, time_started = True, None
          response = None
          req_id = None
          try:
              time_started = time.time()
              _method, _url, _headers = _process_method_url_headers(method, url, headers)
  
              _debug_print_request(_DEBUG, seq_num, time_started, _method, _url, _headers, jsonify_data, data)
  
              body = _maybe_truncate_request(_url, serialized_data)
  
              # throws BadStatusLine if the server returns nothing
              try:
                  pool_manager = _get_pool_manager(**pool_args)
  
                  _headers['User-Agent'] = USER_AGENT
                  _headers['DNAnexus-API'] = API_VERSION
  
                  # Converted Unicode headers to ASCII and throw an error if not possible
                  def ensure_ascii(i):
                      if not isinstance(i, bytes):
                          i = i.encode('ascii')
                      return i
  
                  _headers = {ensure_ascii(k): ensure_ascii(v) for k, v in _headers.items()}
                  if USING_PYTHON2:
                      encoded_url = _url
                  else:
                      # This is needed for python 3 urllib
                      _headers.pop(b'host', None)
                      _headers.pop(b'content-length', None)
  
                      # The libraries downstream (http client) require elimination of non-ascii
                      # chars from URL.
                      # We check if the URL contains non-ascii characters to see if we need to
                      # quote it. It is important not to always quote the path (here: parts[2])
                      # since it might contain elements (e.g. HMAC for api proxy) containing
                      # special characters that should not be quoted.
                      try:
                          ensure_ascii(_url)
                          encoded_url = _url
                      except UnicodeEncodeError:
                          import urllib.parse
                          parts = list(urllib.parse.urlparse(_url))
                          parts[2] = urllib.parse.quote(parts[2])
                          encoded_url = urllib.parse.urlunparse(parts)
  
                  response = pool_manager.request(_method, encoded_url, headers=_headers, body=body,
                                                  timeout=timeout, retries=False, **kwargs)
  
              except urllib3.exceptions.ClosedPoolError:
                  # If another thread closed the pool before the request was
                  # started, will throw ClosedPoolError
                  raise exceptions.UrllibInternalError("ClosedPoolError")
  
              _raise_error_for_testing(try_index, method)
              req_id = response.headers.get("x-request-id", "unavailable")
  
              if (_UPGRADE_NOTIFY
                 and response.headers.get('x-upgrade-info', '').startswith('A recommended update is available')
                 and '_ARGCOMPLETE' not in os.environ):
                  logger.info(response.headers['x-upgrade-info'])
                  try:
                      with file(_UPGRADE_NOTIFY, 'a'):
                          os.utime(_UPGRADE_NOTIFY, None)
                  except:
                      pass
                  _UPGRADE_NOTIFY = False
  
              # If an HTTP code that is not in the 200 series is received and the content is JSON, parse it and throw the
              # appropriate error.  Otherwise, raise the usual exception.
              if response.status // 100 != 2:
                  # response.headers key lookup is case-insensitive
                  if response.headers.get('content-type', '').startswith('application/json'):
                      try:
                          content = response.data.decode('utf-8')
                      except AttributeError:
                          raise exceptions.UrllibInternalError("Content is none", response.status)
                      try:
                          content = json.loads(content)
                      except ValueError:
                          # The JSON is not parsable, but we should be able to retry.
                          raise exceptions.BadJSONInReply("Invalid JSON received from server", response.status)
                      try:
                          error_class = getattr(exceptions, content["error"]["type"], exceptions.DXAPIError)
                      except (KeyError, AttributeError, TypeError):
                          raise exceptions.HTTPError(response.status, content)
                      raise error_class(content, response.status, time_started, req_id)
                  else:
                      try:
                          content = response.data.decode('utf-8')
                      except AttributeError:
                          raise exceptions.UrllibInternalError("Content is none", response.status)
                      raise exceptions.HTTPError("{} {} [Time={} RequestID={}]\n{}".format(response.status,
                                                                                           response.reason,
                                                                                           time_started,
                                                                                           req_id,
                                                                                           content))
  
              if want_full_response:
                  return response
              else:
                  if 'content-length' in response.headers:
                      if int(response.headers['content-length']) != len(response.data):
                          range_str = (' (%s)' % (headers['Range'],)) if 'Range' in headers else ''
                          raise exceptions.ContentLengthError(
                              "Received response with content-length header set to %s but content length is %d%s. " +
                              "[Time=%f RequestID=%s]" %
                              (response.headers['content-length'], len(response.data), range_str, time_started, req_id)
                          )
  
                  content = response.data
  
                  response_was_json = False
  
                  if decode_response_body:
                      content = content.decode('utf-8')
                      if response.headers.get('content-type', '').startswith('application/json'):
                          try:
                              content = json.loads(content)
                          except ValueError:
                              # The JSON is not parsable, but we should be able to retry.
                              raise exceptions.BadJSONInReply("Invalid JSON received from server", response.status)
                          else:
                              response_was_json = True
  
                  req_id = response.headers.get('x-request-id') or "--"
  
                  _debug_print_response(_DEBUG, seq_num, time_started, req_id, response.status, response_was_json,
                                        _method, _url, content)
  
                  if test_retry:
                      retried_responses.append(content)
                      if len(retried_responses) == 1:
                          continue
                      else:
                          _set_retry_response(retried_responses[0])
                          return retried_responses[1]
  
                  return content
              raise AssertionError('Should never reach this line: expected a result to have been returned by now')
          except Exception as e:
              # Avoid reusing connections in the pool, since they may be
              # in an inconsistent state (observed as "ResponseNotReady"
              # errors).
              _get_pool_manager(**pool_args).clear()
              success = False
              exception_msg = _extract_msg_from_last_exception()
              if isinstance(e, _expected_exceptions):
                  # Total number of allowed tries is the initial try PLUS
                  # up to (max_retries) subsequent retries.
                  total_allowed_tries = max_retries + 1
                  ok_to_retry = False
                  is_retryable = always_retry or (method == 'GET') or _is_retryable_exception(e)
                  # Because try_index is not incremented until we escape
                  # this iteration of the loop, try_index is equal to the
                  # number of tries that have failed so far, minus one.
                  if try_index + 1 < total_allowed_tries:
                      # BadStatusLine ---  server did not return anything
                      # BadJSONInReply --- server returned JSON that didn't parse properly
                      if (response is None
                         or isinstance(e, (exceptions.ContentLengthError, BadStatusLine, exceptions.BadJSONInReply,
                                           urllib3.exceptions.ProtocolError, exceptions.UrllibInternalError))):
                          ok_to_retry = is_retryable
                      else:
                          ok_to_retry = 500 <= response.status < 600
  
                      # The server has closed the connection prematurely
                      if (response is not None
                         and response.status == 400 and is_retryable and method == 'PUT'
                         and isinstance(e, requests.exceptions.HTTPError)):
                          if '<Code>RequestTimeout</Code>' in exception_msg:
                              logger.info("Retrying 400 HTTP error, due to slow data transfer. " +
                                          "Request Time=%f Request ID=%s", time_started, req_id)
                          else:
                              logger.info("400 HTTP error, of unknown origin, exception_msg=[%s]. " +
                                          "Request Time=%f Request ID=%s", exception_msg, time_started, req_id)
                          ok_to_retry = True
  
                      # Unprocessable entity, request has semantical errors
                      if response is not None and response.status == 422:
                          ok_to_retry = False
  
                  if ok_to_retry:
                      if rewind_input_buffer_offset is not None:
                          data.seek(rewind_input_buffer_offset)
  
                      delay = _calculate_retry_delay(response, try_index_including_503 + 1)
  
                      range_str = (' (range=%s)' % (headers['Range'],)) if 'Range' in headers else ''
                      if response is not None and response.status == 503:
                          waiting_msg = 'Waiting %d seconds before retry...' % (delay,)
                      else:
                          waiting_msg = 'Waiting %d seconds before retry %d of %d...' % (
                              delay, try_index + 1, max_retries)
  
                      logger.warning("[%s] %s %s: %s. %s %s",
                                     time.ctime(), method, _url, exception_msg, waiting_msg, range_str)
                      time.sleep(delay)
                      try_index_including_503 += 1
                      if response is None or response.status != 503:
                          try_index += 1
                      continue
  
              # All retries have been exhausted OR the error is deemed not
              # retryable. Print the latest error and propagate it back to the caller.
              if not isinstance(e, exceptions.DXAPIError):
                  logger.error("[%s] %s %s: %s.", time.ctime(), method, _url, exception_msg)
  
              if isinstance(e, urllib3.exceptions.ProtocolError) and \
                  'Connection reset by peer' in exception_msg:
                  # If the protocol error is 'connection reset by peer', most likely it is an
                  # error in the ssl handshake due to unsupported TLS protocol.
                  _test_tls_version()
  
              # Retries have been exhausted, and we are unable to get a full
              # buffer from the data source. Raise a special exception.
              if isinstance(e, urllib3.exceptions.ProtocolError) and \
                 'Connection broken: IncompleteRead' in exception_msg:
                  raise exceptions.DXIncompleteReadsError(exception_msg)
              raise
          finally:
              if success and try_index > 0:
                  logger.info("[%s] %s %s: Recovered after %d retries", time.ctime(), method, _url, try_index)
  
          raise AssertionError('Should never reach this line: should have attempted a retry or reraised by now')
      raise AssertionError('Should never reach this line: should never break out of loop')