Skip to the content.

restcl : A focused REST Client for Modern C++

CodeQL Build Status

Getting started

NOTE We are going to track VS 2022 and make full use of C++20 facilities and the API is subject to change.

Dependencies

We use NuGet for dependencies. Why? Simply put, it is the easiest source for obtaining packages. Publishing your own packages is superior to the manual and as-yet-immature vcpkg. They want me to git clone the thing and build it first… NuGet, comparatively, gives you a first-class experience and writing your own packages is a breeze! Sure, it does not work for non-Windows and I’ll have to eventually tackle CMake.

Package Comments
nlohmann.json
This is one of the simplest JSON libraries for C++.
We have to make choices and this is our choice: clean, simple and elegant over efficiency. Our use-case
The library is quite conformant and lends itself to general purpose use since it uses <vector> underneath it all.
We leave time and experience (plus manpower) to optimize this library for us. So long as it works and we do not have to worry about some JAVA-esque or C-style interface!
azure-cpp-utils
This is library with helpers for encryption, base64 encode/decode and conversion of utf8<–>wide strings.
SplitUri
This is library provides parsing of the url.
string2map
This library provides for parsing of HTTP headers into a std::map
acw32h
This library provides for an auto-closing wrapper for HINTERNET, HSESSION and HINSTANCE objects.
RunOnEnd
This library provides for arbitrary lambda call on scope exit.
asynchrony-lib
Provides for utility to add asynchrony to the restcl library.

Unless otherwise noted, use the latest. We’re quite aggressive updating dependencies.


API

Namespace: siddiqsoft
File: restcl_winhttp.hpp

NOTE Internal helpers have been omitted for clarity and when not used by the client code.

class siddiqsoft::WinHttpRESTClient

This is the starting point for your client. We make extensive use of initializer list and json to make REST calls more JavaScript-like. Just take a look at the example.

Signature

    class WinHttpRESTClient : public basic_restclient
    {
        WinHttpRESTClient(const WinHttpRESTClient&) = delete;
        WinHttpRESTClient& operator=(const WinHttpRESTClient&) = delete;

        WinHttpRESTClient(WinHttpRESTClient&&);
        WinHttpRESTClient(const std::string& ua = {});

        basic_response send(basic_request& req);
        void send(basic_request&& req, basic_callbacktype&& callback);
        void send(basic_request&& req, basic_callbacktype& callback);
    };

Member Variables

Private members are implementation-specific and detailed in the source file.

Member Functions

WinHttpRESTClient::WinHttpRESTClient

    WinHttpRESTClient::WinHttpRESTClient( const std::string& );

Creates the Windows REST Client with given UserAgent string and creates a reusable HSESSION object.

Sets the HTTP/2 option and the decompression options

Parameters

WinHttpRESTClient::send

    basic_response send(basic_request& req);

Uses the existing hSession to connect, send, receive data from the remote server and returns the response in synchronous mode.

Parameters
Parameter Type Description
req basic_request The Request to be sent to the remote server.
return basic_response The Response from the remote server or IO error code and message.

See the examples section.

WinHttpRESTClient::send

    void send(basic_request&& req, basic_callbacktype&& callback);
    void send(basic_request&& req, basic_callbacktype& callback);

Uses the existing hSession to connect, send, receive data from the remote server and fire the callback.

Returns immediately once the request has been queued into the threadpool.

Why no “return object”?

Invoking a lambda and minimize the data-copy as well as lifetime of the underlying objects. The callback has the original request as well as the response as const to minimize data race.

Parameters
Parameter Type Description
req basic_request The Request to be sent to the remote server. The data is required to be moved into the function as it takes ownership of the request lifetime.
callback basic_callbacktype The callback is invoked on completion or an error. There are two versions: you can pass an existing function/member or a lambda.

See the examples section.

alias basic_callbacktype

Signature

    using basic_callbacktype = std::function<void(const basic_request&  req,
                                                  const basic_response& resp)>;

Callback invoked by the library on error / success. The request and response are valid for the lifespan of the call but may not be modified.

Parameters
Parameter Type Description
req const basic_request The Request to be sent to the remote server.
resp const basic_response The Response from the remote server.

class basic_request

Signature

class basic_request
{
protected:
    basic_request();
    explicit basic_request(const std::string& endpoint);
    explicit basic_request(const Uri<char>& s);

public:
    const auto&     operator[](const auto& key) const;
    auto&           operator[](const auto& key);
    basic_request&  setContent(const std::string& contentType, const std::string& content);
    basic_request&  setContent(const nlohmann::json& c);
    std::string     getContent() const;
    void            encodeHeaders_to(std::string& rs) const;
    std::string     encode() const;

    Uri<char, AuthorityHttp<char>> uri;

protected:
    nlohmann::json  rrd;

Member Variables

Parameter Type Description
uri Uri<char,AuthorityHttp<char>> The Uri for this client
rrd nlohmann::json The json contains the request data: {"request": {"method": "GET", "uri": {}, "version": ""}, "headers": {}, "content": nullptr}

The underlying json object has the following structure

Field Type Description
request json Contains the request line decomposed into the key-values
- method - GET, POST, etc.
- url - The path to the document
- version - HTTP/2 or HTTP/1.1

Member Functions

basic_request::operator[] const
    const auto&        operator[](const auto& key) const;

The key can be either std::string or json_pointer type.

Accessor for request, headers and content (returns the key within the underlying json object).

To access the request path: req["request"]["url"] and to access the Content-Type, you’d use req["headers"]["Content-Type"]. You can also use json_pointer notation to access the elements: req["/headers/Content-Type"_json_pointer]

API simplicity. Use an access model that is simple and flexible without the need for adding specific methods

basic_request::operator[]
    auto&              operator[](const auto& key);

The key can be either std::string or json_pointer type.

Mutator for request, headers and content (returns the key within the underlying json object).

To access the request path: req["request"]["url"] and to access the Content-Type, you’d use req["headers"]["Content-Type"]. You can also use json_pointer notation to access the elements: req["/headers/Content-Type"_json_pointer]

basic_request::setContent
    basic_request& setContent(const std::string& contentType,
                              const std::string& content);

Convenience method to set non-JSON content along with the headers Content-Type and Content-Length.

Functionally equivalent to the following:

    req["content"]= content;
    req["headers"]["Content-Type"]= contentType;

If the header Content-Length is not set then the value is calculated during the encode() invocation.

basic_request::setContent
    basic_request& setContent(const nlohmann::json& c);

Convenience method to set non-JSON content along with the headers Content-Type and Content-Length.

Functionally equivalent to the following: req["content"]= content; // where content is json

basic_request::getContent const
    std::string        getContent() const;

Returns a serialized representation of the content.

If the content is json then the method .dump() is invoked to serialized the json.

basic_request::encodeHeaders_to
    void               encodeHeaders_to(std::string& rs) const;

Helper to encode the headers to a given string using std::format and std::back_inserter.

basic_request::encode
    std::string        encode() const;

Helper encodes the HTTP request with request line, header section and the content.


class basic_response

Signature

class basic_response
{
protected:
    basic_response();

public:
    struct response_code
    {
        uint32_t    code {0};
        std::string message {};
    };

    basic_response(const basic_response&);
    basic_response(basic_response&&);

    basic_response& operator=(basic_response&&);
    basic_response& operator=(const basic_response&);
    basic_response& setContent(const std::string&);
    const auto&     operator[](const auto&) const;
    auto&           operator[](const auto&);
    std::string     encode() const;
    bool            success() const;
    response_code   status() const;

protected:
    nlohmann::json rrd { {"response", { {"version", HTTPProtocolVersion::Http2},
                                        {"status", 0},
                                        {"reason", ""}}},
                         {"headers", nullptr},
                         {"content", nullptr} };

basic_response::response_code

Name Type Description
code uint32_t If the IO was successful then the value is the HTTP status code.
message std::string For successful IO this is the reason phrase otherwise it is the WinHTTP error message corresponding to the IO error code.

Member Variables

Parameter Type Description
rrd nlohmann::json The json contains the response data: {"response": {"reason": "OK", "status": 200, "version": ""}, "headers": {}, "content": nullptr}

The internal response_code struct represents the IO error code and IO error message or the HTTP response status and HTTP reason string.

Member Functions

Omit eplanations for constructors. They’re pretty standard default/empty, move constructors and move assignment operator.

basic_response::setContent
    basic_response&              setContent(const std::string& content);

This method should not be used by the client.

The client must use the headers to figure out the type of the content and its length.

basic_response::success
    bool                             success() const;

Returns true if the HTTP status >=99 and <400. False if there is any IO error or status >400 from the remote server.

basic_response::operator[] const
    const auto&        operator[](const auto& key) const;

The key can be either std::string or json_pointer type.

Accessor for response, headers and content (returns the key within the underlying json object).

To access the request path: req["response"]["url"] and to access the Content-Type, you’d use resp["headers"]["Content-Type"]. You can also use json_pointer notation to access the elements: resp["/headers/Content-Type"_json_pointer]

API simplicity. Use an access model that is simple and flexible without the need for adding specific methods

basic_response::operator[]
    auto&              operator[](const auto& key);

The key can be either std::string or json_pointer type.

Mutator for response, headers and content (returns the key within the underlying json object).

To access the request path: resp["request"]["url"] and to access the Content-Type, you’d use resp["headers"]["Content-Type"]. You can also use json_pointer notation to access the elements: req["/headers/Content-Type"_json_pointer]

basic_response::encode
    std::string                      encode() const;
basic_response::status
    response_code status() const;

Returns response_code status/ioerror and the reason-phrase or ioerror message.

Equivalent to resp["response"]["status"] and resp["response"]["reason"] or the WinHTTP error code and the corresponding WinHTTP message.


Examples

GET

    #include "siddiqsoft/restcl.hpp"
    #include "siddiqsoft/restcl_winhttp.hpp"
    ...
    using namespace siddiqsoft;
    using namespace siddiqsoft::splituri_literals;

    WinHttpRESTClient wrc("my-user-agent-string");

    // Create a simple GET request from the endpoint string
    // Send the request and invoke the callback.
    wrc.send( "https://google.com"_GET,
              [](const auto& req, const auto& resp) {
                 if(resp.success())
                    doSomething();
              });

POST

    #include "siddiqsoft/restcl.hpp"
    #include "siddiqsoft/restcl_winhttp.hpp"
    ...
    using namespace siddiqsoft;
    using namespace siddiqsoft::splituri_literals;

    WinHttpRESTClient wrc("my-user-agent-string");
        ...
    // Create a POST request by parsing out the string
    auto myPost= "https://server:999/path?q=hello-world"_POST;
    // Add custom header
    myPost["headers"]["X-MyHeader"]= "my-header-value";
    // Adds the content with supplied json object and sets the 
    // headers Content-Length and Content-Type
    myPost.setContent( { {"foo", "bar"}, {"goto", 99} } );
    // Send the request and invoke the callback
    wrc.send( std::move(myReq), [](auto& req, auto& resp){
                                    if(resp.success())
                                        doSomething();
                                    else
                                        logError(resp.error());
                                });

This is the actual implemenation of the Azure Cosmos REST create-document call:

Our design is data descriptive and makes use of the initializer list, overloads to make the task of creating a REST call simple. Our goal is to allow you to focus on your task and not worry about the underlying IO call (one of the few things that I like about JavaScript’s model).

The code here focusses on the REST API and its structure as required by the Cosmos REST API. You’re not struggling with the library or dealing with yet-another-string class or some convoluted JSON library or a talkative API or legacy C-like APIs.

    /// @brief Create an entity in documentdb using the json object as the payload.
    /// @param dbName Database name
    /// @param collName Collection name
    /// @param doc The document must include the `id` and the partition key.
    /// @return status code and the created document as returned by Cosmos
    CosmosResponseType create(const std::string& dbName, const std::string& collName, const nlohmann::json& doc)
    {
        ...
        CosmosResponseType ret {0xFA17, {}};
        auto               ts   = DateUtils::RFC7231();
        auto               pkId = "siddiqsoft.com";
        auto               auth = EncryptionUtils::CosmosToken<char>(
                                                cnxn.current().Key,
                                                "POST",
                                                "docs",
                                                std::format("dbs/{}/colls/{}", dbName, collName),
                                                ts);

        restClient.send( siddiqsoft::ReqPost {
                            std::format("{}dbs/{}/colls/{}/docs",
                                        cnxn.current().currentWriteUri(),
                                        dbName,
                                        collName),
                            { {"Authorization", auth},
                             {"x-ms-date", ts},
                             {"x-ms-documentdb-partitionkey", nlohmann::json {pkId}},
                             {"x-ms-version", config["apiVersion"]},
                             {"x-ms-cosmos-allow-tentative-writes", "true"} },
                            doc},
                         [&ret](const auto& req, const auto& resp) {
                            ret = {std::get<0>(resp.status()), resp.success() ? std::move(resp["content"])
                                                                              : nlohmann::json {}};
                       });

        return ret;
    }

Notes