Skip to content

shlomnissan/express-client

Repository files navigation

Promised-based HTTP client for modern C++ development.

Ubuntu MacOS Windows

codecov

Table of Contents

Overview

Express Client is a promise-based HTTP client for modern C++ development. It currently targets C++20 with no dependencies other than the standard library. The project aims to provide a modern interface for making HTTP requests in C++ applications.

Features

  • A simple interface.
  • Asynchronous requests.
  • Cross-platform support.
  • Basic HTTP authentication.
  • Zero dependencies.
  • Comprehensive tests.

Upcoming Features

  • HTTPS support.
  • File upload and download with progress.
  • Connection pooling.

Getting Started

Platform Support

Express Client supports both Windows and Unix-based operating systems. It is currently being tested on the following operating systems and compilers:

Ubuntu Ubuntu 22.04 and GCC 11.3.0

macOS macOS 12 and Clang 14.0.0

Windows Windows 10 and MSVC 19.36

Installation

Express Client uses CMake as its default build system. There are multiple ways to import a CMake project into an existing codebase, but it is highly recommended to import the library using CMake Install following the steps below. This ensures that symbol visibility is maintained, which makes versioning and future updates more reliable.

1. Installing CMake

The first step is to ensure that CMake is installed on your system. Follow the CMake installation steps for your operating system and run the following command from the terminal to verify the installation. If CMake was installed successfully, you should see similar output:

/$ cmake --version
-- cmake version 3.26.4
-- CMake suite maintained and supported by Kitware (kitware.com/cmake).

Express Client requires a minimum version of CMake 3.22.

2. Download

The next step is downloading the Express Client source code. The easiest way to download Express Client is cloning the repository from GitHub:

  /$ git clone https://github.com/shlomnissan/express-client.git

You can also download an archive file directly from the GitHub UI.

3. Build

To build Express Client using CMake, navigate to the project's root directory and execute the following commands:

# Create the build directory
/$ mkdir build
/$ cd build

# Generate the build
/$ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF

The CMake CLI used to generate the build specifies several modifiable options:

  • CMAKE_BUILD_TYPE is set to Release, which is desirable for installation. However, if you are actively testing and modifying the project, you can change this value to Debug.
  • BUILD_SHARED_LIBS is set to ON, which makes the build output a shared library. Omitting this option altogether results in building a static library.
  • BUILD_TESTS and BUILD_EXAMPLES are self-explanatory.

The next step is building the project:

# Build the project
/$ cmake --build . --config Release

We are specifying the build configuration again which is required for multi-config build systems. If you changed the value to Debug during the build phase, make sure to change it here as well.

4. Install

Finally, you can use CMake to install the library on your host.

# Install the project
/$ cmake --install .

Assuming everything went well, you should now have Express Client installed locally and can include it directly in your projects. If you're on a Unix-based system, running the following command will list the installed files and their respective paths:

/$ cat install_manifest.txt

The next section shows a simple example of using Express Client.

Example

The following example shows how to perform a simple HTTP GET request in your C++ projects and print the output to stdout.

#include <iostream>
#include <express/client.h>

auto main() -> int {
  Express::Client client;

  auto result = client.Request({
    .url = "http://example.com/",
    .method = Express::Method::Get,
  });

  auto response = result.get();

  if (response.status_code == 200) {
    std::cout << response.data << '\n';
  }

  return 0;
}

Now you can compile and run the executable.

# Compile main.cc and link the express client library
/$ g++ main.cc -lexpress_client -std=c++20

# Run the executable
/$ ./a.out
  • Note that all of the examples in this article are using Designated Initializers, which is a feature introduced in C++20. This feature enables us to directly initialize members of a class type using their name.
  • We include the <express/client.h> header file and link the executable with libexpress_client. If you have followed the installation steps, the header files and library should be available in your system's default search paths.

Troubleshooting

  • If you have followed the steps to install the library locally and are seeing the error message "error while loading shared libraries" when running the executable, you may need to update your local shared library cache by running the following command:
/$ ldconfig

Express Client API

Request

The Express Client interface has a single operation, which is making an HTTP request. Therefore, every interaction with the library begins by calling the Request() method on the client's object and passing it a configuration object.

#include <express/client.h>

namespace Express {
  class Client {
    auto Request(const Config& config) const -> std::future<Response>;
  };
}
  • This method processes the request in the background without blocking execution. It returns a std::future, which lets you access the result of the asynchronous operation.
  • std::future provides several methods for querying, waiting for, or extracting a value. The most commonly used method is std::future<T>::get(), which waits until the future has a valid result and retrieves it. If the future does not have a result, this method will block the execution, waiting until the result becomes available.
  • Here is a code snippet from the example we showed earlier with clarifying comments:
// Instantiate our HTTP client
Express::Client client;

// Send an asynchronous request to http://example.com/
auto result = client.Request({
  .url = "http://example.com/",
  .method = Express::Method::Get,
});

// Get the response if it's available, otherwise wait until it's available
auto response = result.get();

The following section will describe the different types provided by the Express Client. We will start with the configuration object that is used to make requests, which includes all the options that can be set when making an HTTP request.

Types

Express::Config

Every request requires a configuration object of type Express::Config. This is a simple data type with fields that lets you configure your HTTP request. We’ve shown two fields in the example above, url which is required, and method. The following table lists all available fields, their types, and a description.

Name Type Description
url std::string_view A valid URL that includes the URL scheme.
method Express::Method An HTTP method supported by Express.
headers Express::Headers A collection of headers for the HTTP request.
data std::string_view Data to include with the request.
auth Express::UserAuth A username and password pair for authentication.
timeout std::chrono::milliseconds A request timeout in milliseconds.

Before we delve into the nested types, let's take a look at an example of an HTTP request that uses all the fields in the configuration object:

auto result = client.Request({
  .url = "http://example.com/user",
  .method = Express::Method::Post,
  .headers = {{
    {"Content-Type", "application/x-www-form-urlencoded"}
  }},
  .data = "age=22&email=myemail@hello.com",
  .auth = {
    .username = "aladdin",
    .password = "opensesame",
  },
  .timeout = 15s
});

Note that when using designated initializers, the initialization expression must have the same order of data members as in the class declaration. However, we are allowed to omit members.

Express::Method

Express::Method is an enum class used to specify the HTTP method within the request object. Express Client currently supports the following methods:

Express::Method::Delete
Express::Method::Get
Express::Method::Head
Express::Method::Options
Express::Method::Patch
Express::Method::Post
Express::Method::Put

Express::Headers

Express::Headers is a collection type used to store headers for a request or retrieve headers from a response. It can be initialized by passing a vector of string pairs, or using an initializer list:

Express::Headers headers {{
  {"Header-Name", "Value"},
  {"Header-Name", "Value"}
}};

This collection object includes a couple of mutators (Express::Headers::Add, Express::Headers::Remove) and accessors (Express::Headers::Contains, Express::Headers::Get) to configure the object as part of an HTTP request and query the object as part of an HTTP response.

auto Add(const std::string& name, std::string value) -> void;
auto Remove(const std::string& name) -> void;

[[nodiscard]] auto Contains(const std::string& name) const -> bool;
[[nodiscard]] auto Get(const std::string& name) -> std::string;

The headers collection is a simple wrapper around an unordered map. However, it accounts for case-insensitive header names and provides basic validation for allowed characters in the header names and values.

Express::UserAuth

Express::UserAuth is a simple aggregate type used for HTTP authentication. It can be initialized with designated initializers as shown below:

Express::UserAuth user_auth {
  .username = "aladdin",
  .password = "opensesame"
};

This user-defined type is part of the request object. If the username or password are set, the request builder will add an Authorization header with this information encoded in base64, following the specification for Basic HTTP Authentication (RFC 7617).

Response

The type of value we receive from the request method's future is Express::Response. This is another simple data structure that mostly includes standard library types. The table below lists all available fields, their types, and default values.

Name Type Description
status_code int An HTTP response status code.
status_text std::string An HTTP status code text.
data std::string A string that includes the body’s data.
headers Express::Headers A collection of key/value headers from the HTTP response.

The only user-defined type in this data structure is Express::Headers, which was covered in the previous section. Here is an example that makes a simple request and prints the entire response using all the fields in the response object.

auto result = client.Request({
  .url = "http://example.com",
  .method = Express::Method::Get,
});

Express::Response response = result.get();

std::cout << "Status: "
  << response.status_code << " "
  << response.status_text << "\n\n";

std::cout << "Headers: \n";
for (const auto& [_, header] : response.headers) {
  std::cout << header.first << ": " << header.second << '\n';
}

if (!response.data.empty()) {
  std::cout << '\n' << response.data;
}

This code snippet was taken from the hello_client.cc example.

Error Handling

Express Client uses exceptions as its primary error handling mechanism. Your application will need to handle three types of exceptions:

Exception Type Description
std::system_error A standard library exception that indicates a low-level system error, typically in the networking APIs. This error can occur in cases of resource limitations, permission issues, or underlying network issues, such as when the destination is unreachable.
Express::RequestError A library exception that indicates a configuration error preventing the library from making a request. This exception is derived from std::logic_error, and can occur in cases such as a bad URL format, invalid header values, or bad data formatting.
Express::ResponseError A library exception that indicates an unexpected runtime error that happened after the library made a request to the server. This exception is derived from std::runtime_error, and can occur in cases such as failure to connect to the server, timeout, unexpected server responses, and more.
  • The library exceptions are defined in <express/exception.h>.
  • The three exceptions all include error messages that provide information about the source of the error and the issues that caused the exception to be thrown.

Licence

    ____       __                             __  
   / __ )___  / /_____ _____ ___  ____ ______/ /__
  / __  / _ \/ __/ __ `/ __ `__ \/ __ `/ ___/ //_/
 / /_/ /  __/ /_/ /_/ / / / / / / /_/ / /  / ,<   
/_____/\___/\__/\__,_/_/ /_/ /_/\__,_/_/  /_/|_|  
                                                  
Copyright (c) 2023-present Shlomi Nissan
https://betamark.com

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.

About

⚡ Promised-based HTTP client for modern C++ development.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages