/* -*- mode: c++; indent-tabs-mode: nil -*- */
/*
    QC_HTTPClient.qpp

    Qore Programming Language

    Copyright (C) 2006 - 2023 Qore Technologies, s.r.o.

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.

    Note that the Qore library is released under a choice of three open-source
    licenses: MIT (as above), LGPL 2+, or GPL 2+; see README-LICENSE for more
    information.
*/

#include <qore/Qore.h>
#include <qore/QoreHttpClientObject.h>
#include "qore/intern/QC_HTTPClient.h"
#include "qore/intern/QC_Queue.h"
#include "qore/intern/QC_SocketPollOperation.h"
#include "qore/intern/ssl_constants.h"
#include "qore/minitest.hpp"
#include "qore/QoreSSLCertificate.h"
#include "qore/QoreSSLPrivateKey.h"

extern QoreClass* QC_SOCKET;

#ifdef DEBUG_TESTS
#  include "tests/QC_HTTPClient_tests.cpp"
#endif

//! The HTTPClient class can be used to communicate with HTTP servers with and without TLS/SSL encryption
/** The HTTPClient class can be used to communicate with HTTP servers using the HTTP or HTTPS (HTTP using an SSL/TLS
    encrypted connection) protocol.

    By default \c "Connection: Keep-Alive" is always sent regardless of the HTTP protocol level set for the object,
    however if a server response contains \c "Connection: close", the connection will be closed as soon as the full
    response (including any message body if present) has been read.

    HTTP redirect responses are supported and can be limited with the \c max_redirects constructor hash key or by
    using the HTTPClient::setMaxRedirects() method. The default maximum number of redirects allowed is 5.

    HTTP basic authentication is supported; set the username and password in the URL (ex:
    \c "http://username:password@host:port/path"). To change the URL from the one set by the constructor, call
    HTTPClient::setURL().

    HTTP proxies and basic proxy authentication are supported by setting the proxy constructor hash key to the proxy
    URL (with a proxy username and password if required) or by calling the HTTPClient::setProxyURL() method.

    Objects of this class are thread-safe and support serializing multiple simultaneous requests from many threads. If
    a request is in progress and another thread attempts to make a request at the same time, the second thread will
    block until the first is complete. Therefore the total amount of time a thread could wait for a response in a
    multi-threaded context could be greater than the timeout value (which applies to the maximum time a single
    internal send() or recv() action can take).

    This class understands and automatically decodes \c "deflate", \c "gzip", and \c "bzip2" content encodings as
    well.

    The default connect timeout value is 60,000 milliseconds (60 seconds), and the default I/O timeout is 300,000
    milliseconds (5 minutes). Note that the timeout value applies to individual internal send() or recv() operations;
    for this reason for large transfers the overall I/O time could exceed the timeout value.

    When an exception is thrown (for example, a response code of < \c 100 or >= \c 400 is received from the server),
    any message body returned will be in the \c "arg" key of the exception hash.

    This class understands the protocols in the following table.\n\n
    <b>%HTTPClient Class Protocols</b>
    |!Protocol/Scheme|!Default Port|!SSL?|!Description
    |\c http|\c 80|No|Unencrypted HTTP protocol
    |\c https|\c 443|Yes|HTTP protocol with SSL/TLS encryption

    Whenever using an HTTPClient method where a hash of headers can be passed to the method, some headers are
    generated by default by the class and can be overridden, and some are cannot be overridden and are ignored if
    passed by the client. See the following tables for details.

    <b>%HTTPClient Mandatory Headers</b>
    |!Header|!Description
    |\c Content-Length|This header is only sent if a message body is sent, and, if so, the length is calculated \
        automatically.

    <b>%HTTPClient Default, but Overridable Headers</b>
    |!Header|!Default Value
    |\c Accept|\c "text/html"
    |\c Content-Type|\c "text/html"
    |\c User-Agent|\c "Qore-HTTP-Client/0.8.8"
    |\c Connection|\c "Keep-Alive"
    |\c Accept-Encoding|\c "gzip,deflate,bzip2"

    Note that if the \c Content-Type header is sent without a \c ";charset=xxx" or \c ";boundary=xxxx" specification,
    and a string is used as the message body for the request and the string does not have \c ISO-8859-1 encoding, a
    \c ";charset=xxx" string will be appended to the \c Content-Type header to ensure that the message is received and
    processed correctly on the remote end.  In all other cases the caller is responsible for correctly setting the
    \c Content-Type header including any necessary \c ":charset=xxx" specification.

    This class supports posting network events to a @ref Qore::Thread::Queue "Queue". See @ref event_handling for more
    information.

    The events raised by this object are listed in the following table:
    <b>HTTPClient Events</b>
    |!Name|!Description
    |@ref EVENT_HTTP_CONTENT_LENGTH|Raised when the HTTP \c "Content-Length" header is received.
    |@ref EVENT_HTTP_CHUNKED_START|Raised when HTTP chunked data is about to be received.
    |@ref EVENT_HTTP_CHUNKED_END|Raised when all HTTP chunked data has been received.
    |@ref EVENT_HTTP_REDIRECT|Raised when an HTTP redirect message is received.
    |@ref EVENT_HTTP_SEND_MESSAGE|Raised when an HTTP message is sent.
    |@ref EVENT_HTTP_MESSAGE_RECEIVED|Raised when an HTTP message is received.
    |@ref EVENT_HTTP_FOOTERS_RECEIVED|Raised when HTTP footers are received.
    |@ref EVENT_HTTP_CHUNKED_DATA_RECEIVED|Raised when a block of HTTP chunked data is received.
    |@ref EVENT_HTTP_CHUNK_SIZE|Raised when the next chunk size for HTTP chunked data is known.

    @section httpclient_get_with_body HTTP GET Requests With a Message Body

    The %Qore HTTPClient class allows \c GET requests to be made with a message body, however please note
    that this is a bad practice according to HTTP 1.1 RFCs; specifically:
    - <a href="https://tools.ietf.org/html/rfc2616#section-4.3">RFC 2616 section 4.3</a>:
    @verbatim if the request method does not include defined semantics for an entity-body,
then the message-body SHOULD be ignored when handling the request.@endverbatim
    - <a href="https://tools.ietf.org/html/rfc7231#section-4.3.1">RFC 7231 section 4.3.1</a>:
    @verbatim A payload within a GET request message has no defined semantics; sending a
payload body on a GET request might cause some existing implementations to
reject the request.@endverbatim
    .
    Therefore we can conclude that \c GET methods do not define any semantics for message bodies sent with the
    request, therefore message bodies included with \c GET requests \c SHOULD be ignored according to the HTTP 1.1
    spec, therefore it's recommended not to send a message body with a \c GET request if standards compliance and
    compatibility are goals.

    @note
    - This class is not available with the @ref PO_NO_NETWORK parse option.
    - URLs with UNIX sockets are generally supported in Qore with the following syntax:
      <tt><b>scheme://socket=</b></tt><i>url_encoded_path</i><tt><b>/path</b></tt>, where <i>url_encoded_path</i> is a
      path with URL-encoding as performed by @ref encode_url() "encode_url(string, True)"; for example:
      \c "http://socket=%2ftmp%socket-dir%2fsocket-file-1/url/path"; this allows a filesystem path to be used in the
      host portion of the URL and for the URL to include a URL path as well.
    - HTTP headers should accept data in \c ISO-8859-1 (\c LATIN-1) encoding, but in practice for maximum
      compatibility \c US-ASCII should be used with any non-ASCII characters encoded with MIME encoded-word format
      according to <a href="https://tools.ietf.org/html/rfc2047">RFC 2047</a>.  This class currently only performs
      character encoding conversion on strings used as the message body.
    - The default character encoding for this class is the same as the
      @ref default_encoding "default character encoding" for %Qore (normally \c UTF-8); this class does not include a
      \c ";charset=" specification in the \c "Content-Type" header when sending binary message bodies.

    @since %Qore 1.0 added code to add a \c ";charset=xxx" to HTTP messages with a non-ISO-8859-1 string message body
*/
qclass HTTPClient [dom=NETWORK; arg=QoreHttpClientObject* client; vparent=Socket];

//! Creates the HTTPClient object based on the option parameter passed
/** To connect, call any method that requires a connection and an implicit connection is established, or call
    HTTPClient::connect().

    @par Example:
    @code{.py}
HTTPClient httpclient(("url":"http://hostname:8080/path"));
    @endcode

    @param opts sets options and changes default behaviour for the object, etc; key names are case-sensitive and
    therefore must all be in lower-case:
    - \c additional_methods: An optional hash defining additional HTTP methods to handle.  This allows the HTTPClient
      class to handle various HTTP extensions like e.g. WebDAV. The hash is defined with new method names as keys; the
      values are @ref True "True" or @ref False "False" indicating if the method can accept a message body; for
      example:
    @code{.py}
    # add two new HTTP methods for WebDAV; both require message bodies
    HTTPClient httpclient({"url": url, "additional_methods": {"PROPFIND": True, "MKCOL": True}});
    @endcode
    - \c assume_encoding: assumes the given encoding if the server does not send a charset value
    - \c connect_timeout: The timeout value in milliseconds for establishing a new socket connection (also can be a
      @ref relative_dates "relative date-time value" for clarity, ex: \c 30s)
    - \c default_path: The default path to use for new connections if a path is not otherwise specified in the
      connection URL
    - \c default_port: The default port number to connect to if none is given in the URL
    - \c encode_chars: Additional characters to encode in URI paths
    - \c encoding: The default character encoding for outgoing messge bodies
    - \c encoding_passthru: Do not decode known content-encodings but rather pass the body through as-is
    - \c error_passthru: If @ref True "True" then HTTP status codes indicating errors will not cause an
      \c HTTP-CLIENT-RECEIVE-ERROR exception to be raised, rather such responses will be passed through to the caller
      like any other response
    - \c headers: A hash of headers to send in every outgoing request
    - \c http_version: Either \c "1.0" or \c "1.1" for the claimed HTTP protocol version compliancy in outgoing
      message headers
    - \c max_redirects: The maximum number of redirects before throwing an exception (the default is 5)
    - \c pre_encoded_urls: If @ref True "True" then all URI paths in URLs are assumed to be already
      <a href="https://en.wikipedia.org/wiki/Percent-encoding">percent encoded</a>; if this flag is set and any
      unencoded characters are sent in a URL with a new request, an exception is raised.  Characters that must require
      percent encoding are: <tt>{}|\\^~[]`</tt>
    - \c protocols: A hash describing new protocols, the key is the protocol name and the value is either an integer
      giving the default port number or a hash with \c "port" and \c "ssl" keys giving the default port number and a
      boolean value to indicate that an SSL connection should be established
    - \c proxy: The proxy URL for connecting through a proxy
    - \c redirect_passthru: if @ref True "True" then redirect responses will be passed to the caller instead of
      processed
    - \c ssl_cert_data: a binary (DER) or string (PEM) value representing the certificate; if this option is used,
      then \c ssl_cert_path is ignored
    - \c ssl_cert_path: a path to an X.509 client certificate file in PEM format; if this option is used, then the
      calling context must not be restricted with sandbox restriction @ref Qore::PO_NO_FILESYSTEM which is checked at
      runtime
    - \c ssl_key_data: a binary (DER) or string (PEM) value representing the private key; if this option is used,
      then \c ssl_key_path is ignored
    - \c ssl_key_path: a path to a private key file in PEM format for the X.509 client certificate; if this option is
      used, then the calling context must not be restricted with sandbox restriction @ref Qore::PO_NO_FILESYSTEM which
      is checked at runtime
    - \c ssl_key_password: the password to the private key given with \c ssl_key_path
    - \c ssl_verify_cert: if @ref True "True" then the server's certificate will only be accepted if it's verified
    - \c timeout: The timeout value in milliseconds (also can be a @ref relative_dates "relative date-time value" for
      clarity, ex: \c 5m)
    - \c url: A string giving the URL to connect to

    @throw HTTP-CLIENT-OPTION-ERROR invalid or unknown option passed in option hash
    @throw HTTP-CLIENT-URL-ERROR invalid URL string
    @throw HTTP-CLIENT-UNKNOWN-PROTOCOL unknown protocol passed in URL
    @throw ILLEGAL-FILESYSTEM-ACCESS if the calling context is restricted with the @ref Qore::PO_NO_FILESYSTEM
    sandboxing restriction and one of the following options is used: \c ssl_cert_path or \c ssl_key_path

    @note
    - URLs with UNIX sockets are generally supported in Qore with the following syntax:
      <tt><b>scheme://socket=</b></tt><i>url_encoded_path</i><tt><b>/path</b></tt>, where <i>url_encoded_path</i> is a
      path with URL-encoding as performed by @ref encode_url() "encode_url(string, True)"; for example:
      \c "http://socket=%2ftmp%socket-dir%2fsocket-file-1/url/path"; this allows a filesystem path to be used in the
      host portion of the URL and for the URL to include a URL path as well.
    - Other I/O errors can be thrown opening and reading the certificate and/or private key files if the
      \c ssl_cert_path or \c ssl_key_path options are used
    - The @ref Qore::PO_NO_FILESYSTEM sandbox restriction is checked at runtime if one of the following options is
      used: \c ssl_cert_path or \c ssl_key_path

    @since
    - %Qore 0.8.13 added the following options:
      - \c ssl_cert_path
      - \c ssl_key_path
      - \c ssl_key_password
      - \c ssl_verify_cert
    - %Qore 0.9.3 added the following options:
      - \c encoding_passthru
      - \c error_passthru
      - \c redirect_passthru
    - %Qore 0.9.4.2 added the following option:
      - \c assume_encoding
    - %Qore 0.9.4.6 added the following option:
      - \c encoding
    - %Qore 0.9.5 added the following option:
      - \c headers
    - %Qore 1.10 added the following options:
      - \c ssl_cert_data
      - \c ssl_key_data
    - %Qore 1.13 added the following option:
      - \c pre_encoded_urls
    - %Qore 1.19 added the following option:
      - \c encode_chars
 */
HTTPClient::constructor(hash<auto> opts) {
    ReferenceHolder<QoreHttpClientObject> client(new QoreHttpClientObject, xsink);
    if (client->setOptions(opts, xsink)) {
        return;
    }

    self->setPrivate(CID_HTTPCLIENT, client.release());
}

//! Creates the HTTPClient object
/**
    @par Example:
    @code{.py}
HTTPClient httpclient();
    @endcode
 */
HTTPClient::constructor() {
    self->setPrivate(CID_HTTPCLIENT, new QoreHttpClientObject);
}

//! Copying objects of this class is not supported, an exception will be thrown
/**
    @throw HTTPCLIENT-COPY-ERROR copying HTTPClient objects is not yet supported
 */
HTTPClient::copy() {
    xsink->raiseException("HTTPCLIENT-COPY-ERROR", "copying HTTPClient objects is not yet supported");
}

//! Destroys the HTTPClient object and closes any open connections
/**
    @par Example:
    @code{.py}
delete httpclient;
    @endcode
 */
HTTPClient::destructor() {
    // have to clear callbacks before destroying
    client->cleanup(xsink);
    client->deref(xsink);
}

//! Returns an @ref Qore::AbstractPollOperation "AbstractPollOperation" object to make a non-blocking HTTP connection
/** @return an @ref Qore::AbstractPollOperation "AbstractPollOperation" object to make a non-blocking HTTP connection

    @since %Qore 1.12
*/
AbstractPollOperation HTTPClient::startPollConnect() {
    return client->startPollConnect(xsink, self);
}

//! Sets the HTTP protocol version string for headers in outgoing messages, allowed values are \c "1.0" and \"1.1\".
/** @param ver \c "1.0" or \"1.1\" for the HTTP protocol compliance version

    @par Example:
    @code{.py}
httpclient.setHTTPVersion("1.1");
    @endcode

    @throw HTTP-VERSION-ERROR invalid HTTP version passed (allowed values: \c "1.0" and \"1.1\")
 */
nothing HTTPClient::setHTTPVersion(string ver) {
    client->setHTTPVersion(ver->c_str(), xsink);
}

//! Returns the HTTP protocol version string used in outgoing messages
/** @return the HTTP protocol version string used in outgoing messages

    @par Example:
    @code{.py}
string version = httpclient.getHTTPVersion();
    @endcode
 */
string HTTPClient::getHTTPVersion() [flags=CONSTANT] {
    return new QoreStringNode(client->getHTTPVersion());
}

//! Sets the object to make a secure SSL/TLS connection on the next connect if the passed argument is @ref True, or an unencrypted cleartext connection if it is @ref False
/** This method overrides the default behaviour for the protocol set for the object

    Note that the behavior of this method when called with no argument changed in version 0.8.0; prior to version 0.8.0 calling this method with no argument would turn off secure mode; the behavior was changed to the current functionality in order to make the usage of this method consistent with other methods of the same name and to make it more logical.

    @par Example:
    @code{.py}
httpclient.setSecure(True);
    @endcode

    @param secure if @ref True, a SSL/TLS connection will be attempted on the next connection. If @ref False, an unencrypted cleartext connection will be established
 */
nothing HTTPClient::setSecure(softbool secure = True) {
    client->setSecure(secure);
}

//! Returns @ref True if the current connection is encrypted, @ref False if not
/** @return @ref True if the current connection is encrypted, @ref False if not

    @par Example:
    @code{.py}
if (httpclient.isSecure())
    printf("secure connection: %s %s\n", httpclient.getSSLCipherName(), httpclient.getSSLCipherVersion());
    @endcode
 */
bool HTTPClient::isSecure() [flags=CONSTANT] {
    return client->isSecure();
}

//! Connects to the remote socket; SSL/TLS negotiation is performed if required
/** If the protocol indicates that a secure connection should be established (or HTTPClient::setSecure() was called
    previsouly), SSL/TLS negotiation will be attempted.

    If the \c TCP_NODELAY flag has been set (see HTTPClient::setNoDelay()), then after a successful connection to the
    remote socket, this option will be set on the socket. If an error occurs setting the \c TCP_NODELAY option, the
    internal flag is set to false (use HTTPClient::getNoDelay() to check the flag's state) and the error code can be
    retrieved with errno().

    @par Example:
    @code{.py}
httpclient.connect();
    @endcode

    @par Events:
    @ref EVENT_CONNECTING, @ref EVENT_CONNECTED, @ref EVENT_HOSTNAME_LOOKUP, @ref EVENT_HOSTNAME_RESOLVED,
    @ref EVENT_START_SSL, @ref EVENT_SSL_ESTABLISHED

    @note For possible exceptions, see the Socket::connect() method (or Socket::connectSSL() for secure connections).
 */
nothing HTTPClient::connect() {
    client->connect(xsink);
}

//! Disconnects from the remote socket if a connection is established (otherwise does nothing)
/**
    @par Example:
    @code{.py}
httpclient.disconnect();
    @endcode
 */
nothing HTTPClient::disconnect() {
    client->disconnect();
}

//! Returns an @ref Qore::AbstractPollOperation "AbstractPollOperation" object to send a message and receive the response
/**
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE",
    \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a
    \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param body The message body to send; note that sending an HTTP message bosdy with a \c GET request is not
    standards compliant; see @ref httpclient_get_with_body for more information
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is
    converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last
    header in the hash with a matching name with a case-insensitive search

    @return an @ref Qore::AbstractPollOperation "AbstractPollOperation" object to send a message and receive the
    response; the @ref Qore::AbstractPollOperation::getOutput() "AbstractPollOperation::getOutput()" method returns
    a hash with the following keys after a successful request and response:
    - \c code: the HTTP response code returned
    - \c info: a hash of information about the request with the following keys:
      - \c request-uri: a string giving the request line (ex: <tt>PUT / HTTP/1.1</tt>)
      - \c response-uri: a string giving the response line (ex: <tt>HTTP/1.1 200 OK</tt>)
      - \c headers-raw: a hash of raw response headers received
      - \c charset: the character encoding used in the response
      - [\c body-content-type]: the MIME \c Content-Type for the response body (if any)
      - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
        each header in the response message, plus the following %Qore-provided keys:
        - \c http_version: the HTTP version in the response URI
        - \c status_code: the HTTP status code of the response
        - \c status_message: the HTTP status message of the response
    - \c request-headers: a hash of headers sent in the request
    - [\c response-body]: a binary object of any response body

    @since %Qore 1.12
*/
AbstractPollOperation HTTPClient::startPollSendRecv(string method, *string path, string body, *hash<auto> headers) {
    return client->startPollSendRecv(xsink, self, method, path, body, body->c_str(), body->size(), headers,
        body->getEncoding());
}

//! Returns an @ref Qore::AbstractPollOperation "AbstractPollOperation" object to send a message and receive the response
/**
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE",
    \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a
    \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param body The message body to send; note that sending an HTTP message bosdy with a \c GET request is not
    standards compliant; see @ref httpclient_get_with_body for more information
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is
    converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last
    header in the hash with a matching name with a case-insensitive search

    @return an @ref Qore::AbstractPollOperation "AbstractPollOperation" object to send a message and receive the
    response; the @ref Qore::AbstractPollOperation::getOutput() "AbstractPollOperation::getOutput()" method returns
    a hash with the following keys after a successful request and response:
    - \c code: the HTTP response code returned
    - \c info: a hash of information about the request with the following keys:
      - \c request-uri: a string giving the request line (ex: <tt>PUT / HTTP/1.1</tt>)
      - \c response-uri: a string giving the response line (ex: <tt>HTTP/1.1 200 OK</tt>)
      - \c headers-raw: a hash of raw response headers received
      - \c charset: the character encoding used in the response
      - [\c body-content-type]: the Content-Type for the response body (if any)
      - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
        each header in the response message, plus the following %Qore-provided keys:
        - \c http_version: the HTTP version in the response URI
        - \c status_code: the HTTP status code of the response
        - \c status_message: the HTTP status message of the response
    - \c request-headers: a hash of headers sent in the request
    - [\c response-body]: a binary object of any response body

    @since %Qore 1.12
*/
AbstractPollOperation HTTPClient::startPollSendRecv(string method, *string path, *binary body, *hash<auto> headers) {
    return client->startPollSendRecv(xsink, self, method, path, body, body ? body->getPtr() : nullptr,
        body ? body->size() : 0, headers);
}

//! Sends an HTTP request with the specified method and optional message body
/** If a connection has not already been established, an internal call to HTTPClient::connect() will be made before
    sending the message

    @par Example:
    @code{.py}
hash<auto> msg = httpclient.send(body, "POST", "/path", {"Content-Type": "application/x-yaml"});
    @endcode

    @param body The message body to send; note that sending an HTTP message bosdy with a \c GET request is not
    standards compliant; see @ref httpclient_get_with_body for more information
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE",
    \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a
    \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is
    converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last
    header in the hash with a matching name with a case-insensitive search
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no
    \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies
    without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request
    headers and other information about the HTTP request; this hash contains the following keys:
    - \c [accept-charset]: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of
      \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to
      the first requested character encoding in the list
    - \c [charset]: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in
      this key
    - \c [body-content-type]: the MIME \c Content-Type for the response body (if any)
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c [request-body]: the raw message body in the request (before any content encoding)
    - \c [response-body]: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
      each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return The headers received from the HTTP server with all key names converted to lower-case. The message body (if
    any) will be assigned to the value of the \c "body" key and the HTTP status will be assigned to the
    \c "status_code" key.

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout
    period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or
    Socket::connectSSL() for secure connections)

    @see @ref httpclient_get_with_body

    @since %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
hash<auto> HTTPClient::send(string body, string method, *string path, *hash<auto> headers, softbool getbody = False, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<QoreHashNode> rv(client->send(method->c_str(), path && !path->empty() ? path->c_str() : nullptr,
        headers, *body, getbody, *ohrh, xsink), xsink);
    return *xsink ? QoreValue() : rv.release();
}

//! Sends an HTTP request with the specified method and optional message body and returns headers and any body received as a response in a hash format
/** If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
hash<auto> msg = httpclient.send(body, "POST", "/path", {"Content-Type": "application/x-yaml"});
    @endcode

    @param body The message body to send; pass @ref nothing (no value) to send no body
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c [accept-charset]: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of
      \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to
      the first requested character encoding in the list
    - \c [charset]: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in
      this key
    - \c [body-content-type]: the MIME \c Content-Type for the response body (if any)
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c [request-body]: the raw message body in the request (before any content encoding)
    - \c [response-body]: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
      each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return The headers received from the HTTP server with all key names converted to lower-case. The message body (if any) will be assigned to the value of the \c "body" key and the HTTP status will be assigned to the \c "status_code" key.

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @see @ref httpclient_get_with_body

    @since %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
hash<auto> HTTPClient::send(*binary body, string method, *string path, *hash<auto> headers, softbool getbody = False, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<QoreHashNode> rv(client->send(method->c_str(), path && !path->empty() ? path->c_str() : nullptr,
        headers, body ? body->getPtr() : nullptr, body ? body->size() : 0, getbody, *ohrh, xsink), xsink);
    return *xsink ? QoreValue() : rv.release();
}

//! Sends an HTTP request with the specified method and chunked message body as given by a send callback and returns headers and any body received as a response in a hash format
/** If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
hash<auto> msg = httpclient.sendWithSendCallback(callback, "POST", "/path", {"Content-Type": "application/x-yaml"});
    @endcode

    @param scb The callback giving the chunked HTTP data to send; this callback must return either a string or a binary value each time it is called to give the chunked data to send; when all data has been sent, then a hash of message trailers can be sent or simply @ref nothing which will close the chunked message
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param timeout_ms the timeout in milliseconds for the socket I/O operations; 0 means use the default timeout value
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c [accept-charset]: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of
      \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to
      the first requested character encoding in the list
    - \c [charset]: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in
      this key
    - \c [body-content-type]: the MIME \c Content-Type for the response body (if any)
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c [request-body]: the raw message body in the request (before any content encoding)
    - \c [response-body]: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
      each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return The headers received from the HTTP server with all key names converted to lower-case. The message body (if any) will be assigned to the value of the \c "body" key and the HTTP status will be assigned to the \c "status_code" key.

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note
    - The \c "Transfer-Encoding: chunked" header is automatically set with this method if no \c "Transfer-Encoding" header is already present
    - For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @see @ref httpclient_get_with_body

    @since
    - %Qore 0.8.10
    - %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
hash<auto> HTTPClient::sendWithSendCallback(code scb, string method, *string path, *hash<auto> headers, timeout timeout_ms = 0, softbool getbody = False, *reference<hash> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<QoreHashNode> rv(client->sendWithSendCallback(method->c_str(), path && !path->empty() ? path->c_str() : 0, headers, scb, getbody, *ohrh, timeout_ms, xsink), xsink);
    return *xsink ? QoreValue() : rv.release();
}

//! Sends an HTTP request with the specified method and optional message body; headers and any body received are returned through a receive callback
/** This method is useful for receiving chunked message data in real time; chunks are sent to the receive callback as soon as they are received.
    If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
httpclient.sendWithRecvCallback(rcv_callback, data, "POST", "/path", {"Content-Type": "application/x-yaml"});
    @endcode

    @param rcb The receive callback for the data received; first this method is called with a hash of the message headers, and then with any message body; if a chunked HTTP message is received, then the callback is called once for each chunk; when the message has been received, then the receive callback is called with a hash representing any trailer data received in a chunked transfer or @ref nothing if the data was received in a normal message body or if there was no trailer data in a chunked transfer.  The argument to this callback is always a hash; data calls have the following keys:
    - \c "data": the string or binary data
    - \c "chunked": True if the data was received with chunked transfer encoding, False if not
    .
    Header or trailer data is placed in a hash with the following keys:
    - \c "hdr": this can be assigned to @ref nothing for the trailer hash if the data was not sent chunked or no trailers were included in a chunked message
    - \c "info": this hash is passed when headers are received; in this case, the hash has at least the following keys:
      - \c "accept-charset": the value of any \c "Accept-Charset" header
      - \c "accept-encoding": the value of any \c "Accept-Encoding" header
      - \c "body-content-type": the value of the \c "Content-Type" header, if any, without any character set info
      - \c "charset": any character set value found in the \c "Content-Type" header
      - \c "response-headers-raw": a hash of unprocessed headers with no case conversions or additional keys
    - \c "obj": this is the owning object (so socket parameters can be changed based on headers received, such as, for example, socket character encoding)
    @param body The message body to send
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param timeout_ms the timeout in milliseconds for the socket I/O operations; 0 means use the default timeout value
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c [accept-charset]: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of
      \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to
      the first requested character encoding in the list
    - \c [charset]: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in
      this key
    - \c [body-content-type]: the MIME \c Content-Type for the response body (if any)
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c [request-body]: the raw message body in the request (before any content encoding)
    - \c [response-body]: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
      each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @see @ref httpclient_get_with_body

    @since
    - %Qore 0.8.10
    - %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
nothing HTTPClient::sendWithRecvCallback(code rcb, string body, string method, *string path, *hash<auto> headers, timeout timeout_ms = 0, softbool getbody = False, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    // issue 1813: convert string to socket's character encoding as documented
    TempEncodingHelper btmp(body, client->getEncoding(), xsink);
    if (*xsink)
        return QoreValue();
    client->sendWithRecvCallback(method->c_str(), path && !path->empty() ? path->c_str() : 0, headers, btmp->c_str(), btmp->size(), getbody, *ohrh, timeout_ms, rcb, self, xsink);
}

//! Sends an HTTP request with the specified method and optional message body; headers and any body received are returned through a receive callback
/** This method is useful for receiving chunked message data in real time; chunks are sent to the receive callback as soon as they are received.
    If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
httpclient.sendWithRecvCallback(rcv_callback, data, "POST", "/path", {"Content-Type": "application/x-yaml"});
    @endcode

    @param rcb The receive callback for the data received; first this method is called with a hash of the message headers, and then with any message body; if a chunked HTTP message is received, then the callback is called once for each chunk; when the message has been received, then the receive callback is called with a hash representing any trailer data received in a chunked transfer or @ref nothing if the data was received in a normal message body or if there was no trailer data in a chunked transfer.  The argument to this callback is always a hash; data calls have the following keys:
    - \c "data": the string or binary data
    - \c "chunked": True if the data was received with chunked transfer encoding, False if not
    .
    Header or trailer data is placed in a hash with the following keys:
    - \c "hdr": this can be assigned to @ref nothing for the trailer hash if the data was not sent chunked or no trailers were included in a chunked message
    - \c "info": this hash is passed when headers are received; in this case, the hash has at least the the following keys:
      - \c "accept-charset": the value of any \c "Accept-Charset" header
      - \c "accept-encoding": the value of any \c "Accept-Encoding" header
      - \c "body-content-type": the value of the \c "Content-Type" header, if any, without any character set info
      - \c "charset": any character set value found in the \c "Content-Type" header
      - \c "response-headers-raw": a hash of unprocessed headers with no case conversions or additional keys
    - \c "obj": this is the owning object (so socket parameters can be changed based on headers received, such as, for example, socket character encoding)
    @param body The message body to send; pass @ref nothing (no value) to send no body
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param timeout_ms the timeout in milliseconds for the socket I/O operations; 0 means use the default timeout value
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c [accept-charset]: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of
      \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to
      the first requested character encoding in the list
    - \c [charset]: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in
      this key
    - \c [body-content-type]: the MIME \c Content-Type for the response body (if any)
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c [request-body]: the raw message body in the request (before any content encoding)
    - \c [response-body]: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
      each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @since
    - %Qore 0.8.10
    - %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
nothing HTTPClient::sendWithRecvCallback(code rcb, *binary body, string method, *string path, *hash<auto> headers, timeout timeout_ms = 0, softbool getbody = False, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    client->sendWithRecvCallback(method->c_str(), path && !path->empty() ? path->c_str() : nullptr, headers, body ? body->getPtr() : nullptr, body ? body->size() : 0, getbody, *ohrh, timeout_ms, rcb, self, xsink);
}

//! Sends an HTTP request with the specified method and optional message body; headers are returned through an optional receive callback and any body received is written to the @ref OutputStream
/** This method is useful for receiving data in real time; data are sent to the output stream as soon as they are received.
    If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
# save the HTTP response body directly to a file with a random file name
string fn = tmp_location() + DirSep + get_random_string() + ".tmp"
FileOutputString output_stream(fn);
# log the "X-Logme" header
code rcb = sub (hash<auto> h) {
    if (h.hdr."x-logme")
        log("X-Logme: %y", h.hdr."x-logme");
};
httpclient.send(output_stream, data, "POST", "/path", {"Content-Type": "application/x-yaml"}, NOTHING, NOTHING, NOTHING, rcb);
    @endcode

    @param os The @ref OutputStream to write the response body to
    @param body The message body to send; pass @ref nothing (no value) to send no body
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param timeout_ms the timeout in milliseconds for the socket I/O operations; 0 means use the default timeout value
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request.
    @param rcb An optional receive callback for message headers received only; header or trailer data is placed in a hash with the following keys:
    - \c "hdr": this can be assigned to @ref nothing for the trailer hash if the data was not sent chunked or no trailers were included in a chunked message
    - \c "info": this hash is passed when headers are received; in this case, the hash has at least the the following keys:
      - \c "accept-charset": the value of any \c "Accept-Charset" header
      - \c "accept-encoding": the value of any \c "Accept-Encoding" header
      - \c "body-content-type": the value of the \c "Content-Type" header, if any, without any character set info
      - \c "charset": any character set value found in the \c "Content-Type" header
      - \c "response-headers-raw": a hash of unprocessed headers with no case conversions or additional keys
    - \c "obj": this is the owning object (so socket parameters can be changed based on headers received, such as, for example, socket character encoding)

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True" (see the
    \c error_passthru option in @ref HTTPClient::constructor()) or if a receive callback is used
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @since %Qore 0.8.13
 */
nothing HTTPClient::send(Qore::OutputStream[OutputStream] os, *data body, string method, *string path, *hash<auto> headers, timeout timeout_ms = 0, softbool getbody = False, *reference<hash<auto>> info, *code rcb) {
    SimpleRefHolder<OutputStream> osHolder(os);
    OptHashRefHelper ohrh(info, xsink);
    const char* pstr = path && !path->empty() ? path->c_str() : nullptr;
    if (get_node_type(body) == NT_STRING) {
        client->sendWithOutputStream(method->c_str(), pstr, headers, *static_cast<const QoreStringNode*>(body), getbody,
            *ohrh, timeout_ms, rcb, self, os, xsink);
    } else {
        const BinaryNode* b = reinterpret_cast<const BinaryNode*>(body);
        client->sendWithOutputStream(method->c_str(), pstr, headers, b ? b->getPtr() : nullptr, b ? b->size() : 0,
            getbody, *ohrh, timeout_ms, rcb, self, os, xsink);
    }
}

//! Sends a chunked HTTP request with the specified method and message body; headers are returned through an optional receive callback and any body received is written to the @ref OutputStream
/** This method is useful for receiving data in real time; data are sent to the output stream as soon as they are received.
    If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
# save the HTTP response body directly to a file with a random file name
string fn = tmp_location() + DirSep + get_random_string() + ".tmp"
FileOutputString output_stream(fn);
# log the "X-Logme" header
code rcb = sub (hash<auto> h) {
    if (h.hdr."x-logme")
        log("X-Logme: %y", h.hdr."x-logme");
};
# write the "X-Content-Done" header when done
code tcb = *hash<auto> sub () {
    return {"X-Content-Done": "true"};
};
httpclient.sendChunked(output_stream, input_stream, "POST", "/path", {"Content-Type": "application/x-yaml"}, NOTHING, NOTHING, NOTHING, rcb, tcb);
    @endcode

    @param os The @ref OutputStream to write the response body to
    @param is The @ref InputStream to read the request body from; each read will be sent as a separate chunk
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param max_chunk_size the maximum chunk size as the read block size for the @ref InputStream read block size; the actual size of the data read will determine the size of each HTTP chunk
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param timeout_ms the timeout in milliseconds for the socket I/O operations; 0 means use the default timeout value
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request.
    @param tcb An optional trailer callback for message trailers to write in the outgoing chunked message; the callback takes no arguments and must return either @ref nothing or @ref hash_type "hash" giving a hash of trailers for the final chunk
    @param rcb An optional receive callback for message headers received only; header or trailer data is placed in a hash with the following keys:
    - \c "hdr": this can be assigned to @ref nothing for the trailer hash if the data was not sent chunked or no trailers were included in a chunked message
    - \c "info": this hash is passed when headers are received; in this case, the hash has at least the the following keys:
      - \c "accept-charset": the value of any \c "Accept-Charset" header
      - \c "accept-encoding": the value of any \c "Accept-Encoding" header
      - \c "body-content-type": the value of the \c "Content-Type" header, if any, without any character set info
      - \c "charset": any character set value found in the \c "Content-Type" header
      - \c "response-headers-raw: a hash of unprocessed headers with no case conversions or additional keys
    - \c "obj": this is the owning object (so socket parameters can be changed based on headers received, such as, for example, socket character encoding)
    @param tcb An optional trailer callback for message trailers to write in the outgoing chunked message; the callback takes no arguments and must return either @ref nothing or @ref hash_type "hash" giving a hash of trailers for the final chunk

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True" (see the
    \c error_passthru option in @ref HTTPClient::constructor()) or if a receive callback is used
    @throw HTTP-CHUNK-ERROR the chunk size given is not greater than 0
    @throw HTTP-TRAILER-ERROR the trailer callback must return either @ref nothing or @ref hash_type "hash"; this exception is thrown is another type is returned
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @since %Qore 0.8.13
 */
nothing HTTPClient::sendChunked(Qore::OutputStream[OutputStream] os, Qore::InputStream[InputStream] is, string method,
        int max_chunk_size = 4096, *string path, *hash<auto> headers, timeout timeout_ms = 0,
        softbool getbody = False, *reference<hash<auto>> info, *code rcb, *code tcb) {
    SimpleRefHolder<OutputStream> osHolder(os);
    SimpleRefHolder<InputStream> isHolder(is);
    if (max_chunk_size < 1) {
        xsink->raiseException("HTTP-CHUNK-ERROR", "the max chunk size must be greater than 0; value given is: " QLLD,
            max_chunk_size);
        return QoreValue();
    }
    OptHashRefHelper ohrh(info, xsink);
    client->sendChunked(method->c_str(), path && !path->empty() ? path->c_str() : nullptr, headers, getbody, *ohrh, timeout_ms, rcb, self, os, is, (size_t)max_chunk_size, tcb, xsink);
}

//! Sends an HTTP request with the specified method and chunked message body as given by a send callback; headers and any body received are returned through a receive callback
/** This method is useful for sending chunked message data where the response is also sent with chunked transfer encoding; chunks are sent to the receive callback as soon as they are received.
    If a connection has not already been established, an internal call to HTTPClient::connect() will be made before sending the message

    @par Example:
    @code{.py}
httpclient.sendWithCallbacks(send_callback, rcv_callback, "POST", "/path", {"Content-Type": "application/x-yaml"});
    @endcode

    @param scb The callback giving the chunked HTTP data to send; this callback must return either a string or a binary value each time it is called to give the chunked data to send; when all data has been sent, then a hash of message trailers can be sent or simply @ref nothing which will close the chunked message
    @param rcb The receive callback for the data received; first this method is called with a hash of the message headers, and then with any message body; if a chunked HTTP message is received, then the callback is called once for each chunk; when the message has been received, then the receive callback is called with a hash representing any trailer data received in a chunked transfer or @ref nothing if the data was received in a normal message body or if there was no trailer data in a chunked transfer.  The argument to this callback is always a hash; data calls have the following keys:
    - \c "data": the string or binary data
    - \c "chunked": True if the data was received with chunked transfer encoding, False if not
    .
    Header or trailer data is placed in a hash with the following keys:
    - \c "hdr": this can be assigned to @ref nothing for the trailer hash if the data was not sent chunked or no trailers were included in a chunked message
    - \c "info": this hash is passed when headers are received; in this case, the hash has at least the following keys:
      - \c "accept-charset": the value of any \c "Accept-Charset" header
      - \c "accept-encoding": the value of any \c "Accept-Encoding" header
      - \c "body-content-type": the value of the \c "Content-Type" header, if any, without any character set info
      - \c "charset": any character set value found in the \c "Content-Type" header
      - \c "response-headers-raw": a hash of unprocessed headers with no case conversions or additional keys
    - \c "obj": this is the owning object (so socket parameters can be changed based on headers received, such as, for example, socket character encoding)
    - \c "send_aborted": this is set to @ref True "True" if a response header was set while sending an outgoing chunked message
    @param method The name of the HTTP method (\c "GET", \c "POST", \c "HEAD", \c "OPTIONS", \c "PUT", \c "DELETE", \c "TRACE", \c "CONNECT", or \c "PATCH"). Additional methods can be added in the constructor with the a \c additional_methods option.
    @param path The path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param timeout_ms the timeout in milliseconds for the socket I/O operations; 0 means use the default timeout value
    @param getbody If this argument is @ref True, then the object will try to receive a message body even if no \c "Content-Length" header is present in the response. Use this only with broken servers that send message bodies without a \c "Content-Length" header.
    @param info An optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c [accept-charset]: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of
      \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to
      the first requested character encoding in the list
    - \c [charset]: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in
      this key
    - \c [body-content-type]: the MIME \c Content-Type for the response body (if any)
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c [request-body]: the raw message body in the request (before any content encoding)
    - \c [response-body]: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for
      each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @throw HTTP-CLIENT-METHOD-ERROR invalid/unknown HTTP method passed
    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received
    @throw URL-ENCODING-ERROR Thrown if the \c pre_encoded_urls option is set, and the URL has at least one unencoded
    character that requires URL percent encoding

    @note
    - The \c "Transfer-Encoding: chunked" header is automatically set with this method if no \c "Transfer-Encoding" header is already present
    - if a response is received while the chunked send operation is still in progress, an error is assumed, the send operation is aborted, and the response header is read immediately and the \c send_aborted flag is set in the argument to \a rcb
    - For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @see @ref httpclient_get_with_body

    @since
    - %Qore 0.8.10
    - %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
nothing HTTPClient::sendWithCallbacks(code scb, code rcb, string method, *string path, *hash<auto> headers,
        timeout timeout_ms = 0, softbool getbody = False, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    client->sendWithCallbacks(method->c_str(), path && !path->empty() ? path->c_str() : nullptr, headers, scb,
        getbody, *ohrh, timeout_ms, rcb, self, xsink);
}

//! Sends an HTTP \c GET request and returns the message body received as a string or @ref nothing if no message body is received
/** In order to get the headers and the body, use the HTTPClient::send() method instead (although note that sending an HTTP message bosdy with a \c GET request is not standards compliant; see @ref httpclient_get_with_body for more information).

    If no connection has already been established, an internal call to HTTPClient::connect() will be made before sending the request.

    If any content encoding is used for the message body in the reply, the content is decoded and returned as a string; if the content encoding uses an unknown method, then an exception is thrown.

    @par Example:
    @code{.py}
*string html = httpclient.get("/path/file.html");
    @endcode

    @param path the path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param info an optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c accept-charset: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to the first requested character encoding in the list
    - \c charset: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in this key
    - \c body-content-type: the outgoing message body MIME \c Content-Type value
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c request-body: the raw message body in the request (before any content encoding)
    - \c response-body: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return the message body in the reply to this message or @ref nothing in case of an error or an erroneous reply by the server with no body

    @par Events:
    @ref EVENT_CONNECTING, @ref EVENT_CONNECTED, @ref EVENT_HOSTNAME_LOOKUP, @ref EVENT_HOSTNAME_RESOLVED, @ref EVENT_START_SSL, @ref EVENT_SSL_ESTABLISHED, @ref EVENT_HTTP_SEND_MESSAGE, @ref EVENT_PACKET_SENT, @ref EVENT_HTTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_HTTP_CONTENT_LENGTH, @ref EVENT_HTTP_CHUNKED_START, @ref EVENT_HTTP_CHUNKED_END, @ref EVENT_HTTP_CHUNKED_DATA_RECEIVED, @ref EVENT_HTTP_CHUNK_SIZE, @ref EVENT_HTTP_FOOTERS_RECEIVED, @ref EVENT_HTTP_REDIRECT

    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @see @ref httpclient_get_with_body

    @since %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
*string HTTPClient::get(string path, *hash<auto> headers, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<AbstractQoreNode> rv(client->get(path->c_str(), headers, *ohrh, xsink), xsink);
    return *xsink ? nullptr : rv.release();
}

//! Sends an HTTP \c HEAD request and returns as hash of the headers received
/** If no connection is established, an internal call to HTTPClient::connect() will be made before sending the message.

    @par Example:
    @code{.py}
hash<auto> msg = httpclient.head("/path");
    @endcode

    @param path the path for the message (i.e. \c "/path/resource?method&param=value")
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param info an optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c accept-charset: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to the first requested character encoding in the list
    - \c charset: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in this key
    - \c body-content-type: the outgoing message body MIME \c Content-Type value
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c request-body: the raw message body in the request (before any content encoding)
    - \c response-body: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return the headers received from the HTTP server with all key names converted to lower-case

    @par Events:
    @ref EVENT_CONNECTING, @ref EVENT_CONNECTED, @ref EVENT_HOSTNAME_LOOKUP, @ref EVENT_HOSTNAME_RESOLVED, @ref EVENT_START_SSL, @ref EVENT_SSL_ESTABLISHED, @ref EVENT_HTTP_SEND_MESSAGE, @ref EVENT_PACKET_SENT, @ref EVENT_HTTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_HTTP_REDIRECT

    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @since %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
hash<auto> HTTPClient::head(string path, *hash<auto> headers, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<QoreHashNode> rv(client->head(path->c_str(), headers, *ohrh, xsink), xsink);
    return *xsink ? nullptr : rv.release();
}

//! Sends an HTTP \c POST request with a message body and returns the message body received as a string or @ref nothing if no message body is received
/** In order to get the headers and the body in the response, use the HTTPClient::send() method instead.

    If no connection is established, an internal call to HTTPClient::connect() will be made before sending the message.

    @par Example:
    @code{.py}
httpclient.post("/path", body);
    @endcode

    @param path the path for the message (i.e. \c "/path/resource?method&param=value")
    @param body the string to use as the message body
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param info an optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c accept-charset: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to the first requested character encoding in the list
    - \c charset: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in this key
    - \c body-content-type: the outgoing message body MIME \c Content-Type value
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c request-body: the raw message body in the request (before any content encoding)
    - \c response-body: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return the message body in the reply to this message or @ref nothing in case no body was present in the response

    @par Events:
    @ref EVENT_CONNECTING, @ref EVENT_CONNECTED, @ref EVENT_HOSTNAME_LOOKUP, @ref EVENT_HOSTNAME_RESOLVED, @ref EVENT_START_SSL, @ref EVENT_SSL_ESTABLISHED, @ref EVENT_HTTP_SEND_MESSAGE, @ref EVENT_PACKET_SENT, @ref EVENT_HTTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_HTTP_CONTENT_LENGTH, @ref EVENT_HTTP_CHUNKED_START, @ref EVENT_HTTP_CHUNKED_END, @ref EVENT_HTTP_CHUNKED_DATA_RECEIVED, @ref EVENT_HTTP_CHUNK_SIZE, @ref EVENT_HTTP_FOOTERS_RECEIVED, @ref EVENT_HTTP_REDIRECT

    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw ENCODING-CONVERSION-ERROR the given string could not be converted to the socket's character encoding
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @since %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
*string HTTPClient::post(string path, string body, *hash<auto> headers, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<AbstractQoreNode> rv(client->post(path->c_str(), headers, *body, *ohrh, xsink), xsink);
#ifdef DEBUG
    assert(!rv || rv->getType() == NT_STRING);
#endif
    return *xsink ? QoreValue() : rv.release();
}

//! Sends an HTTP \c POST request with a message body and returns the message body received as a string or @ref nothing if no message body is received
/** In order to get the headers and the body in the response, use the HTTPClient::send() method instead.

    If no connection is established, an internal call to HTTPClient::connect() will be made before sending the message.

    @par Example:
    @code{.py}
httpclient.post("/path", body);
    @endcode

    @param path the path for the message (i.e. \c "/path/resource?method&param=value")
    @param body the optional data to use as the message body
    @param headers An optional hash of headers to include in the message; values are converted to strings; a list is converted to a string of comma-separated values; headers that differ only in case will be overwritten by the last header in the hash with a matching name with a case-insensitive search
    @param info an optional reference to an lvalue that will be used as an output variable giving a hash of request headers and other information about the HTTP request; this hash contains the following keys:
    - \c accept-charset: this key will be set to an appropriate value from any \c "Accept-Charset" header; if any of \c "*", \c "utf8", or \c "utf-8" are present, then this will be set to \c "utf8", otherwise it will be set to the first requested character encoding in the list
    - \c charset: if there is a \c "charset" declaration in the \c "Content-Type" header, the value is returned in this key
    - \c body-content-type: the outgoing message body MIME \c Content-Type value
    - \c headers: a hash of outgoing HTTP request headers (returned as sent, without any case conversions)
    - \c request-uri: the request URI string sent (ex: \c "GET /services/async/38.0/job HTTP/1.1")
    - \c request-body: the raw message body in the request (before any content encoding)
    - \c response-body: the raw message body in the response (after any content decoding)
    - \c response-headers: a hash of incoming HTTP headers in the response with keys converted to all lower case for each header in the response message, plus the following %Qore-provided keys:
      - \c http_version: the HTTP version in the response URI
      - \c status_code: the HTTP status code of the response
      - \c status_message: the HTTP status message of the response
    - \c response-headers-raw: a hash of raw unprocessed incoming HTTP headers
    - \c response-uri: the HTTP response URI

    @return the message body in the reply to this message or @ref nothing in case no body was present in the response

    @par Events:
    @ref EVENT_CONNECTING, @ref EVENT_CONNECTED, @ref EVENT_HOSTNAME_LOOKUP, @ref EVENT_HOSTNAME_RESOLVED, @ref EVENT_START_SSL, @ref EVENT_SSL_ESTABLISHED, @ref EVENT_HTTP_SEND_MESSAGE, @ref EVENT_PACKET_SENT, @ref EVENT_HTTP_MESSAGE_RECEIVED, @ref EVENT_PACKET_READ, @ref EVENT_HTTP_CONTENT_LENGTH, @ref EVENT_HTTP_CHUNKED_START, @ref EVENT_HTTP_CHUNKED_END, @ref EVENT_HTTP_CHUNKED_DATA_RECEIVED, @ref EVENT_HTTP_CHUNK_SIZE, @ref EVENT_HTTP_FOOTERS_RECEIVED, @ref EVENT_HTTP_REDIRECT

    @throw HTTP-CLIENT-REDIRECT-ERROR invalid redirect location given by remote
    @throw HTTP-CLIENT-MAXIMUM-REDIRECTS-EXCEEDED maximum redirect count exceeded
    @throw HTTP-CLIENT-RECEIVE-ERROR unknown content encoding received or status error communicating with HTTP server
    (status code < 100 or > 299); in case of a status error the \c "arg" key of the exception hash will be set to a
    hash equal to the normal return value of this method including a \c "status_code" key (giving the status code) and
    a \c "body" key (giving the message body returned by the server); note that this error is not raised for HTTP
    status codes indicating an error if the \c error_passthru option is set to @ref True "True"; see the
    \c error_passthru option in @ref HTTPClient::constructor()
    @throw SOCKET-SEND-ERROR There was an error sending the data
    @throw SOCKET-CLOSED The remote end closed the connection
    @throw SOCKET-RECV-ERROR There was an error receiving the data
    @throw SOCKET-TIMEOUT Data transmission or reception for a single send() or recv() action exceeded the timeout period
    @throw SOCKET-SSL-ERROR There was an SSL error while reading data from the socket
    @throw SOCKET-HTTP-ERROR Invalid HTTP data was received

    @note For possible exceptions when implicitly establishing a connection, see the Socket::connect() method (or Socket::connectSSL() for secure connections)

    @since %Qore 0.9.3 added support for the \c response-headers-raw info key
 */
*string HTTPClient::post(string path, *binary body, *hash<auto> headers, *reference<hash<auto>> info) {
    OptHashRefHelper ohrh(info, xsink);
    ReferenceHolder<AbstractQoreNode> rv(client->post(path->c_str(), headers, body ? body->getPtr() : 0, body ? body->size() : 0, *ohrh, xsink), xsink);
#ifdef DEBUG
    assert(!rv || rv->getType() == NT_STRING);
#endif
    return *xsink ? nullptr : rv.release();
}

//! Sets the default I/O timeout value in milliseconds
/** @param timeout_ms 0 means immediate timeout (when reading will return data only if it is already available), and negative numbers mean never timeout. Note that like all %Qore functions and methods taking timeout values, a @ref relative_dates "relative date/time value" can be used to make the units clear (i.e. \c 2m = two minutes, etc.).

    @par Example:
    @code{.py}
httpclient.setTimeout(2m);
    @endcode
 */
nothing HTTPClient::setTimeout(timeout timeout_ms = 0) {
   client->setTimeout((int)timeout_ms);
}

//! Returns the default I/O timeout as an integer in milliseconds
/** @return the default I/O timeout as an integer in milliseconds; 0 means immediate timeout (when reading only returns data if it is already available), and negative numbers mean never timeout

    @par Example:
    @code{.py}
int timeout = httpclient.getTimeout();
    @endcode
 */
int HTTPClient::getTimeout() [flags=CONSTANT] {
   return client->getTimeout();
}

//! Sets the string encoding for the object; any strings deserialized with this object will be tagged with this character encoding
/** @param encoding the string encoding for the object; any strings deserialized with this object will be tagged with this character encoding

    @par Example:
    @code{.py}
httpclient.setEncoding("UTF-8");
    @endcode
 */
nothing HTTPClient::setEncoding(string encoding) {
    client->setEncoding(QEM.findCreate(encoding));
}

//! Returns the character encoding used for the object
/** @return the character encoding used for the object

    @par Example:
    @code{.py}
string encoding = httpclient.getEncoding();
    @endcode
 */
string HTTPClient::getEncoding() [flags=CONSTANT] {
   return new QoreStringNode(client->getEncoding()->getCode());
}

//! Sets a new URL value for the next connection
/** To retrieve the current URL value, use the HTTPClient::getURL() method

    @par Example:
    @code{.py}
httpclient.setURL("https://user:password@hostname:8080/path");
    @endcode

    @param url the new URL for the object

    @throw HTTP-CLIENT-URL-ERROR invalid URL string; invalid authorization credentials in  URL (username without password or vice-versa)
    @throw HTTP-CLIENT-UNKNOWN-PROTOCOL unknown protocol (scheme) passed in URL

    @see HTTPClient::getURL()

    @note URLs with UNIX sockets are generally supported in Qore with the following syntax: <tt><b>scheme://socket=</b></tt><i>url_encoded_path</i><tt><b>/path</b></tt>, where <i>url_encoded_path</i> is a path with URL-encoding as performed by @ref encode_url() "encode_url(string, True)"; for example: \c "http://socket=%2ftmp%socket-dir%2fsocket-file-1/url/path"; this allows a filesystem path to be used in the host portion of the URL and for the URL to include a URL path as well.
 */
HTTPClient::setURL(string url) {
    TempEncodingHelper nurl(url, QCS_DEFAULT, xsink);
    if (*xsink) {
        return QoreValue();
    }
    client->setURL(nurl->c_str(), xsink);
}

//! Returns the current URL
/** @return the current URL with the scheme, address, port (if not the default) and also including any username,
    password, and path

    @par Example:
    @code{.py}
*string url = httpclient.getURL();
    @endcode

    @see getSafeURL()
 */
*string HTTPClient::getURL() [flags=CONSTANT] {
    return client->getURL();
}

//! Returns the current URL without the password field
/** @return the current URL with the scheme, address, port (if not the default) and also including any username and
    path, but without any password

    @par Example:
    @code{.py}
*string url = httpclient.getSafeURL();
    @endcode

    @see getURL()

    @since %Qore 1.6.0
 */
*string HTTPClient::getSafeURL() [flags=CONSTANT] {
    return client->getSafeURL();
}

//! Clears the new proxy URL value for the next connection
/** This variant of the method is equivalent to HTTPClient::clearProxyURL()

    @par Example:
    @code{.py}
httpclient.setProxyURL();
    @endcode
 */
nothing HTTPClient::setProxyURL() {
    client->clearProxyURL();
}

//! Sets a new proxy URL value for the next connection
/** @param url the new proxy URL value for the next connection

    @par Example:
    @code{.py}
httpclient.setProxyURL("http://user:password@proxy_host:8080/path");
    @endcode

    @throw HTTP-CLIENT-URL-ERROR invalid proxy URL string; invalid authorization credentials in proxy URL (username without password or vice-versa)
    @throw HTTP-CLIENT-PROXY-PROTOCOL-ERROR unknown protocol passed in URL

    @note URLs with UNIX sockets are generally supported in Qore with the following syntax: <tt><b>scheme://socket=</b></tt><i>url_encoded_path</i><tt><b>/path</b></tt>, where <i>url_encoded_path</i> is a path with URL-encoding as performed by @ref encode_url() "encode_url(string, True)"; for example: \c "http://socket=%2ftmp%socket-dir%2fsocket-file-1/url/path"; this allows a filesystem path to be used in the host portion of the URL and for the URL to include a URL path as well.
 */
nothing HTTPClient::setProxyURL(string url) {
    TempEncodingHelper nurl(url, QCS_DEFAULT, xsink);
    if (*xsink) {
        return QoreValue();
    }
    client->setProxyURL(nurl->c_str(), xsink);
}

//! Returns the current proxy URL as a string or @ref nothing if no proxy URL is set
/** @return the current proxy URL as a string or @ref nothing if no proxy URL is set

    @par Example:
    @code{.py}
*string proxy_url = httpclient.getProxyURL();
    @endcode

    @see getSafeProxyURL()
*/
*string HTTPClient::getProxyURL() [flags=CONSTANT] {
    return client->getProxyURL();
}

//! Returns the current proxy URL without the password field
/** @return the current proxy URL with the scheme, address, port (if not the default) and also including any username and
    path, but without any password

    @par Example:
    @code{.py}
*string url = httpclient.getSafeProxyURL();
    @endcode

    @see getProxyURL()

    @since %Qore 1.6.0
*/
*string HTTPClient::getSafeProxyURL() [flags=CONSTANT] {
    return client->getSafeProxyURL();
}

//! Clears the new proxy URL value for the next connection
/**
    @par Example:
    @code{.py}
httpclient.setProxyURL();
    @endcode
*/
nothing HTTPClient::clearProxyURL() {
    client->clearProxyURL();
}

//! Sets the SSL/TLS flag for the next connection to the proxy
/**
    @par Example:
    @code{.py}
httpclient.setProxySecure(True);
    @endcode

    @see HTTPClient::isProxySecure() to check the flag
*/
nothing HTTPClient::setProxySecure(softbool b = True) {
    client->setProxySecure(b);
}

//! Returns any username set for the connection
/** @return any username set for the connection

    @since %Qore 1.18.2
*/
*string HTTPClient::getUsername() [flags=CONSTANT] {
    return client->getUsername();
}

//! Returns any password set for the connection
/** @return any password set for the connection

    @since %Qore 1.18.2
*/
*string HTTPClient::getPassword() [flags=CONSTANT] {
    return client->getPassword();
}

//! Returns any username set for the proxy connection, if any
/** @return any username set for the proxy connection, if any

    @since %Qore 1.18.2
*/
*string HTTPClient::getProxyUsername() [flags=CONSTANT] {
    return client->getProxyUsername();
}

//! Returns any password set for the proxy connection, if any
/** @return any password set for the proxy connection, if any

    @since %Qore 1.18.2
*/
*string HTTPClient::getProxyPassword() [flags=CONSTANT] {
    return client->getProxyPassword();
}

//! Returns the SSL/TLS flag for the next proxy connection
/** @return the SSL/TLS flag for the next proxy connection

    @par Example:
    @code{.py}
bool b = httpclient.isProxySecure();
    @endcode
*/
bool HTTPClient::isProxySecure() [flags=CONSTANT] {
    return client->isProxySecure();
}

//! Updates the setting for the \c max_redirects value for the object (maximum number of HTTP redirects that will be processed before an exception is raised)
/** @param mr the setting for the maximum number of HTTP redirects that will be processed before an exception is raised

    @par Example:
    @code{.py}
httpclient.setMaxRedirects(5);
    @endcode

    @see HTTPClient::getMaxRedirects() to retrieve this value
*/
nothing HTTPClient::setMaxRedirects(softint mr = 0) {
    client->setMaxRedirects((int)mr);
}

//! Returns the current \c max_redirects value for the object (the maximum number of HTTP redirects that will be processed before an exception is raised)
/** @return the current \c max_redirects value for the object (the maximum number of HTTP redirects that will be processed before an exception is raised)

    @par Example:
    @code{.py}
int mr = httpclient.getMaxRedirects();
    @endcode
*/
int HTTPClient::getMaxRedirects() [flags=CONSTANT] {
    return client->getMaxRedirects();
}

//! Clears any Queue object that may be set on the HTTPClient object so that @ref event_handling "I/O events" are no longer captured on the object
/**
    @par Example:
    @code{.py}
httpclient.setEventQueue();
    @endcode
*/
nothing HTTPClient::setEventQueue() {
    client->setEventQueue(xsink, nullptr, QoreValue(), false);
}

//! Sets a Queue object to receive HTTPClient and Socket events
/** @par Example:
    @code{.py}
httpclient.setEventQueue(queue);
    @endcode

    @param queue a @ref Qore::Thread::Queue "Queue" object to receive HTTPClient and Socket events; note that the
    Queue passed cannot have any maximum size set or a QUEUE-ERROR will be thrown
    @param arg an argument that will be included in each event hash in the \a arg key
    @param with_data if @ref True, then the actual raw data transferred / received is also included in the events

    @throw QUEUE-ERROR the Queue passed has a maximum size set

    @see event_handling for more information

    @since %Qore 0.9.4 added the \a arg and \a with_data options
*/
nothing HTTPClient::setEventQueue(Qore::Thread::Queue[Queue] queue, auto arg, *bool with_data) {
    ReferenceHolder<Queue> q(queue, xsink);
    if (queue->getMax() != -1)
        return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which "
            "could cause internal I/O to freeze in the HttpClient object if used; use a Queue object with no maximum "
            "size for I/O monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");
    // pass reference from QoreObject::getReferencedPrivateData() to function
    client->setEventQueue(xsink, q.release(), arg.refSelf(), with_data);
}

//! Sets the connect timeout in milliseconds
/** @param timeout_ms the connect timeout in milliseconds; negative numbers mean use the default system connect
    timeout. Note that like all %Qore functions and methods taking timeout values, a
    @ref relative_dates "relative date/time value" can be used to make the units clear (i.e. \c 30s = 30 seconds,
    etc.)

    @par Example:
    @code{.py}
httpclient.setConnectTimeout(2m);
    @endcode
*/
nothing HTTPClient::setConnectTimeout(timeout timeout_ms = -1) {
   client->setConnectTimeout((int)timeout_ms);
}

//! Returns the connect timeout as an integer in milliseconds
/** @return Returns the connect timeout as an integer in milliseconds; negative numbers mean the system default
    timeout is used

    @par Example:
    @code{.py}
int to = httpclient.getConnectTimeout();
    @endcode
*/
int HTTPClient::getConnectTimeout() [flags=CONSTANT] {
   return client->getConnectTimeout();
}

//! Sets the \c TCP_NODELAY setting for the object
/** When this setting is True, then data will be immediately sent out over the HTTPClient object's socket, when it is
    @ref False, then data transmission may be delayed to be packaged with other data for the same target.

    Delayed data transmissions may cause problems when the sender immediately closes the socket after sending data; in
    this case the receiver may not get the data even though the send succeeded.

    Note that if no value is given to the method, the argument will be assumed to be @ref True, and output buffering
    will be turned off for the HTTPClient object.

    If the socket is not connected when this call is made, then an internal flag is set and the \c TCP_NODELAY option
    is enabled when the next connection is established. If the socket is connected, then if an error occurs setting
    the \c TCP_NODELAY option on the socket, this method will return a non-zero error code; the actual error can be
    checked with the errno() function.

    @par Example:
    @code{.py}
httpclient.setNoDelay(True);
    @endcode

    @param b the \c TCP_NODELAY setting for the object

    @see HTTPClient::getNoDelay()
*/
int HTTPClient::setNoDelay(softbool b = True) {
    return client->setNoDelay(b);
}

//! Returns the \c TCP_NODELAY setting for the HTTPClient object
/**
    @par Example:
    @code{.py}
bool b = httpclient.getNoDelay();
    @endcode

    @see also HTTPClient::setNoDelay()
*/
bool HTTPClient::getNoDelay() [flags=CONSTANT] {
    return client->getNoDelay();
}

//! Returns @ref True or @ref False giving the current connection state
/** @return @ref True or @ref False giving the current connection state

    @par Example:
    @code{.py}
bool b = httpclient.isConnected();
    @endcode
*/
bool HTTPClient::isConnected() [flags=CONSTANT] {
    return client->isConnected();
}

//! Sets the username and password for the connection; call after HTTPClient::setURL()
/** Call this method after calling HTTPClient::setURL() to set authentication information when not present in the URL
    used in HTTPClient::setURL()

    @param user the username to use for authentication in the next HTTP connection
    @param pass the password to use for authentication in the next HTTP connection

    @par Example:
    @code{.py}
httpclient.setUserPassword(user, pass);
    @endcode
 */
nothing HTTPClient::setUserPassword(string user, string pass) {
    TempEncodingHelper nuser(user, QCS_DEFAULT, xsink);
    if (*xsink) {
        return QoreValue();
    }
    TempEncodingHelper npass(pass, QCS_DEFAULT, xsink);
    if (*xsink) {
        return QoreValue();
    }

    client->setUserPassword(nuser->c_str(), npass->c_str());
}

//! Clears the username and password for the connection
/** Call this method after calling HTTPClient::setURL() to clear any authentication information present in the URL
    used in HTTPClient::setURL()

    This variant of the method is equivalent to HTTPClient::clearUserPassword()

    @par Example:
    @code{.py}
httpclient.setUserPassword();
    @endcode

    @see HTTPClient::clearUserPassword()
*/
nothing HTTPClient::setUserPassword() {
   client->clearUserPassword();
}

//! Clears the username and password for the connection
/** Call this method after calling HTTPClient::setURL() to clear any authentication information present in the URL
    used in HTTPClient::setURL()

    @par Example:
    @code{.py}
httpclient.clearUserPassword();
    @endcode
*/
nothing HTTPClient::clearUserPassword() {
   client->clearUserPassword();
}

//! Sets the username and password for the connection to the proxy; call after HTTPClient::setProxyURL()
/** Call this method after calling HTTPClient::setProxyURL() to set proxy authentication information when not present
    in the URL used in HTTPClient::setProxyURL()

    @par Example:
    @code{.py}
httpclient.setProxyUserPassword(user, pass);
    @endcode

    @param user the username to use for proxy authentication in the next HTTP connection
    @param pass the password to use for proxy authentication in the next HTTP connection
*/
nothing HTTPClient::setProxyUserPassword(string user, string pass) {
    TempEncodingHelper nuser(user, QCS_DEFAULT, xsink);
    if (*xsink) {
        return QoreValue();
    }
    TempEncodingHelper npass(pass, QCS_DEFAULT, xsink);
    if (*xsink) {
        return QoreValue();
    }

    client->setProxyUserPassword(nuser->c_str(), npass->c_str());
}

//! Clears the username and password for the next proxy connection
/** Call this method after calling HTTPClient::setProxyURL() to clear any proxy authentication information present in
    the URL used in HTTPClient::setProxyURL()

    This variant of the method is equivalent to HTTPClient::clearProxyUserPassword()

    @par Example:
    @code{.py}
httpclient.setProxyUserPassword();
    @endcode

    @see HTTPClient::clearProxyUserPassword()
*/
nothing HTTPClient::setProxyUserPassword() {
    client->clearProxyUserPassword();
}

//! Clears the username and password for the next proxy connection
/** Call this method after calling HTTPClient::setProxyURL() to clear any proxy authentication information present in
   the URL used in HTTPClient::setProxyURL()

    @par Example:
    @code{.py}
httpclient.clearProxyUserPassword();
    @endcode
*/
nothing HTTPClient::clearProxyUserPassword() {
    client->clearProxyUserPassword();
}

//! Sets headers to send by default with every outgoing request
/** @param hdr headers to send by default with every outgoing request

    @see getDefaultHeaders()

    @since %Qore 0.9.5
*/
nothing HTTPClient::addDefaultHeaders(hash<auto> hdr) {
    client->addDefaultHeaders(hdr);
}

//! Returns a hash of default headers to be sent with every outgoing request
/** @return a hash of default headers to be sent with every outgoing request

    @see addDefaultHeaders()

    @since %Qore 0.9.5
*/
hash<string, string> HTTPClient::getDefaultHeaders() [flags=CONSTANT] {
    return client->getDefaultHeaders();
}

//! Sets the default path used by the object if no path is set in the URL
/** @param path the default path value to set or if @ref nothing then clears the path

    @par Example:
    @code{.py}
httpclient.setDefaultPath();
    @endcode
 */
nothing HTTPClient::setDefaultPath(*string path) {
    TempEncodingHelper npath;
    if (path && !npath.set(path, QCS_DEFAULT, xsink)) {
        return QoreValue();
    }
    client->setDefaultPath(path ? npath->c_str() : nullptr);
}

//! Returns the default path used by the object if no path is set in the URL
/** @return the default path used by the object if no path is set in the URL

    @par Example:
    @code{.py}
*string def_path = httpclient.getDefaultPath();
    @endcode
 */
string HTTPClient::getDefaultPath() [flags=CONSTANT] {
   const char* dp = client->getDefaultPath();
   return dp ? new QoreStringNode(dp) : nullptr;
}

//! Returns the current connection path set in the URL
/** @return the current connection path set in the URL

    @par Example:
    @code{.py}
*string path = httpclient.getConnectionPath();
    @endcode

    @see setConnectionPath()
*/
*string HTTPClient::getConnectionPath() [flags=CONSTANT] {
   const char* p = client->getConnectionPath();
   return p ? new QoreStringNode(p) : nullptr;
}

//! Overrides any connection path set in the URL
/** @par Example:
    @code{.py}
httpclient.setConnectionPath(new_path);
    @endcode

    @see getConnectionPath()

    @since %Qore 0.9.3.2
*/
HTTPClient::setConnectionPath(*string uri_path) {
    TempEncodingHelper path;
    if (uri_path && !path.set(uri_path, QCS_DEFAULT, xsink)) {
        return QoreValue();
    }
    client->setConnectionPath(uri_path ? path->c_str() : nullptr);
}

//! Removes any warning @ref Qore::Thread::Queue "Queue" object from the Socket
/** @par Example:
    @code{.py}
httpclient.clearWarningQueue();
    @endcode

    @see HTTPClient::setWarningQueue()

    @since Qore 0.8.9
*/
nothing HTTPClient::clearWarningQueue() {
    client->clearWarningQueue(xsink);
}

//! Sets a @ref Qore::Thread::Queue "Queue" object to receive socket warnings
/** @par Example:
    @code{.py}
httpclient.setWarningQueue(5000, 5000, queue, "socket-1");
    @endcode

    @param warning_ms the threshold in milliseconds for individual socket actions (send, receive, connect), if
    exceeded, a socket warning is placed on the warning queue with the following keys:
    - \c "type": a string with the constant value \c "SOCKET-OPERATION-WARNING"
    - \c "operation": a string giving the operation that caused the warning (ex: \c "connect")
    - \c "us": an integer giving the number of microseconds for the operation
    - \c "timeout": an integer giving the warning threshold in microseconds
    - \c "arg": if any \c "arg" argument is passed to the
      @ref Qore::HTTPClient::setWarningQueue() "HTTPClient::setWarningQueue()" method, it will be included in the
      warning hash here
    @param warning_bs value in bytes per second; if any call has performance below this threshold, a socket warning is
    placed on the warning queue with the following keys:
    - \c "type": a string with the constant value \c "SOCKET-THROUGHPUT-WARNING"
    - \c "dir": either \c "send" or \c "recv" depending on the direction of the data flow
    - \c "bytes": the amount of bytes sent
    - \c "us": an integer giving the number of microseconds for the operation
    - \c "bytes_sec": a float giving the transfer speed in bytes per second
    - \c "threshold": an integer giving the warning threshold in bytes per second
    - \c "arg": if any \c "arg" argument is passed to the
      @ref Qore::HTTPClient::setWarningQueue() "HTTPClient::setWarningQueue()" method, it will be included in the
      warning hash here
    @param queue the @ref Qore::Thread::Queue "Queue" object to receive warning events
    @param arg an optional argument to be placed in the \c "arg" key in each warning hash (could be used to identify
    the socket for example)
    @param min_ms the minimum transfer time with a resolution of milliseconds for a transfer to be eligible for
    triggering a warning; transfers that take less than this period of time are not eligible for raising a warning

    @throw QUEUE-ERROR the Queue passed has a maximum size set
    @throw SOCKET-SETWARNINGQUEUE-ERROR at least one of \a warning_ms and \a warning_bs must be > 0

    @see HTTPClient::clearWarningQueue()

    @since
    - %Qore 0.8.9
    - %Qore 0.8.12 the minimum threshold for a warning is a transfer of at least 1024 bytes, smaller transfers will
      not result in a warning
*/
nothing HTTPClient::setWarningQueue(int warning_ms, int warning_bs, Queue[Queue] queue, auto arg, timeout min_ms = 1s) {
    ReferenceHolder<Queue> q(queue, xsink);
    if (queue->getMax() != -1)
        return xsink->raiseException("QUEUE-ERROR", "the Queue object passed as a maximum size of %d entr%s, which "
            "could cause internal I/O to freeze in the Socket object if used; use a Queue object with no maximum "
            "size for I/O monitoring", queue->getMax(), queue->getMax() == 1 ? "y" : "ies");
    // pass reference from QoreObject::getReferencedPrivateData() to function
    client->setWarningQueue(xsink, warning_ms, warning_bs, q.release(), arg.refSelf(), min_ms);
}

//! Returns performance statistics for the socket
/** @par Example:
    @code{.py}
hash<auto> h = httpclient.getUsageInfo();
    @endcode

    @return a hash with the following keys:
    - \c "bytes_sent": an integer giving the total amount of bytes sent
    - \c "bytes_recv": an integer giving the total amount of bytes received
    - \c "us_sent": an integer giving the total number of microseconds spent sending data
    - \c "us_recv": an integer giving the total number of microseconds spent receiving data
    - \c "arg": (only if warning values have been set with
      @ref Qore::HTTPClient::setWarningQueue() "HTTPClient::setWarningQueue()") the optional argument for warning
      hashes
    - \c "timeout": (only if warning values have been set with
      @ref Qore::HTTPClient::setWarningQueue() "HTTPClient::setWarningQueue()") the warning timeout in microseconds
    - \c "min_throughput": (only if warning values have been set with
      @ref Qore::HTTPClient::setWarningQueue() "HTTPClient::setWarningQueue()") the minimum warning throughput in
      bytes/sec

    @since Qore 0.8.9

    @see HTTPClient::clearStats()
*/
hash<auto> HTTPClient::getUsageInfo() [flags=CONSTANT] {
    return client->getUsageInfo();
}

//! Clears performance statistics
/** @par Example:
    @code{.py}
httpclient.clearStats();
    @endcode

    @since Qore 0.8.9

    @see HTTPClient::getUsageInfo()
*/
HTTPClient::clearStats() {
    client->clearStats();
}

//! temporarily disables implicit reconnections; must be called when the server is already connected
/** @par Example:
    @code{.py}
httpclient.connect();
httpclient.setPersistent();
    @endcode

    The persistent flag is automatically reset to @ref False "False" whenever the connection is closed; it must be
    called manually for every connection to turn off implicit reconnections.

    To turn off the persistent flag manually, call @ref HTTPClient::disconnect()

    @since Qore 0.8.10
*/
HTTPClient::setPersistent() {
    client->setPersistent(xsink);
}

//! set the error passthru status
/** @par Example:
    @code{.py}
httpclient.setErrorPassthru();
    @endcode

    @param set if @ref True "True" then HTTP status codes indicating errors will not cause an
      \c HTTP-CLIENT-RECEIVE-ERROR exception to be raised, rather such responses will be passed through to the caller
      like any other response

    @return the old \c error_passthru value

    If @ref True "True" then HTTP status codes indicating errors will not cause an
    \c HTTP-CLIENT-RECEIVE-ERROR exception to be raised, rather such responses will be passed through to the caller
    like any other response.

    @since %Qore 0.9.3
*/
bool HTTPClient::setErrorPassthru(bool set = True) {
    return client->setErrorPassthru(set);
}

//! get the error passthru status
/** @par Example:
    @code{.py}
bool b = httpclient.getErrorPassthru();
    @endcode

    @return the current \c error_passthru value

    If @ref True "True" then HTTP status codes indicating errors will not cause an
    \c HTTP-CLIENT-RECEIVE-ERROR exception to be raised, rather such responses will be passed through to the caller
    like any other response.

    @since %Qore 0.9.3
*/
bool HTTPClient::getErrorPassthru() [flags=CONSTANT] {
    return client->getErrorPassthru();
}

//! set the redirect passthru status
/** @par Example:
    @code{.py}
httpclient.setRedirectPassthru();
    @endcode

    @param set if @ref True "True" then redirect messages will be passed to the caller instead of proceessed

    @return the old \c reddirect_passthru value

    If @ref True "True" then redirect messages will be passed to the callers instead of proceessed

    @since %Qore 0.9.3
*/
bool HTTPClient::setRedirectPassthru(bool set = True) {
    return client->setRedirectPassthru(set);
}

//! get the redirect passthru status
/** @par Example:
    @code{.py}
bool b = httpclient.getRedirectPassthru();
    @endcode

    @return the current \c reddirect_passthru value

    If @ref True "True" then redirect messages will be passed to the caller instead of proceessed


    @since %Qore 0.9.3
*/
bool HTTPClient::getRedirectPassthru() [flags=CONSTANT] {
    return client->getRedirectPassthru();
}

//! set the encoding passthru status
/** @par Example:
    @code{.py}
httpclient.setEncodingPassthru();
    @endcode

    @param set if @ref True "True" then message bodies received with known content encodings are not decoded but
    rather passed through as-is

    @return the old \c encoding_passthru value

    If @ref True "True" then message bodies received with known content encodings are not decoded but
    rather passed through as-is

    @since %Qore 0.9.3
*/
bool HTTPClient::setEncodingPassthru(bool set = True) {
    return client->setEncodingPassthru(set);
}

//! get the encoding passthru status
/** @par Example:
    @code{.py}
bool b = httpclient.getErrorPassthru();
    @endcode

    @return the current \c encoding_passthru value

    If @ref True "True" then message bodies received with known content encodings are not decoded but
    rather passed through as-is

    @since %Qore 0.9.3
*/
bool HTTPClient::getEncodingPassthru() [flags=CONSTANT] {
    return client->getEncodingPassthru();
}

//! returns the \c Host header value for this object
/** @par Example:
    @code{.py}
string host = httpclient.getHostHeaderValue();
    @endcode

    @return the \c Host header value for this object

    @since %Qore 0.9.3
*/
string HTTPClient::getHostHeaderValue() [flags=CONSTANT] {
    return client->getHostHeaderValue();
}

//! sets the assumed character encoding for messages from the HTTP server without any charset indicator
/** @par Example:
    @code{.py}
httpclient.setAssumedEncoding("utf-8");
    @endcode

    @param encoding the encoding to set; if empty or not present, the default \c "ISO-8859-1" will be set

    @since %Qore 0.9.4.2
*/
HTTPClient::setAssumedEncoding(*string encoding) {
    client->setAssumedEncoding(encoding && !encoding->empty() ? encoding->c_str() : "ISO-8859-1");
}

//! returns the assumed character encoding for messages from the HTTP server without any charset indicator
/** @par Example:
    @code{.py}
string str = httpclient.getAssumedEncoding();
    @endcode

    @return the assumed character encoding for messages from the HTTP server without any charset indicator; by default
    this is \c "ISO-8859-1"

    @since %Qore 0.9.4.2
*/
string HTTPClient::getAssumedEncoding() [flags=RET_VALUE_ONLY] {
    return client->getAssumedEncoding();
}

//! set the pre-encoded URL flag
/** @par Example:
    @code{.py}
httpclient.setPreEncodedUrls();
    @endcode

    @param set if @ref True "True" then all URI paths in URLs are assumed to be already
    <a href="https://en.wikipedia.org/wiki/Percent-encoding">percent encoded</a>; if this flag is set and any
    unencoded characters are sent in a URL with a new request, an exception is raised.

    @return the old \c pre_encoded_urls value

    If @ref True "True" then all URI paths in URLs are assumed to be already
    <a href="https://en.wikipedia.org/wiki/Percent-encoding">percent encoded</a>; if this flag is set and any
    unencoded characters are sent in a URL with a new request, an exception is raised.

    @since %Qore 1.13.0
*/
bool HTTPClient::setPreEncodedUrls(bool set = True) {
    return client->setPreEncodedUrls(set);
}

//! get the pre-encoded URL flag
/** @par Example:
    @code{.py}
bool b = httpclient.getPreEncodedUrls();
    @endcode

    @return the current \c pre_encoded_urls value

    If @ref True "True" then all URI paths in URLs are assumed to be already
    <a href="https://en.wikipedia.org/wiki/Percent-encoding">percent encoded</a>; if this flag is set and any
    unencoded characters are sent in a URL with a new request, an exception is raised.

    @since %Qore 1.13.0
*/
bool HTTPClient::getPreEncodedUrls() [flags=CONSTANT] {
    return client->getPreEncodedUrls();
}
