You're using curl? Do you even lift?
liblifthttp is a C++17 HTTP client library that provides an easy to use API for both synchronous and asynchronous requests. It is built upon the super heavyweight champions libcurl and libuv libraries.
liblifthttp is licensed under the Apache 2.0 license.
- Easy to use Synchronous and Asynchronous HTTP Request APIs.
- Safe C++17 client library API, modern memory move semantics.
- Background IO thread(s) for sending and receiving Async HTTP requests.
- Request pooling for re-using HTTP requests and sharing of connection information.
- libcurl 7.81.0 is unsupported due to a known libcurl bug in the multi handle code. Unfortunately ubuntu 22.04 comes with this version installed by default, you will need to manually install a different version of libcurl or build libcurl from source and link to it to avoid segfaults in asynchronous http requests via liblift. See here for more information
- libcurl share does not work across multiple threads, expect segfaults if you try and use the
lift::share
objects across threads. The bug is apparently very difficult to fix and is unlikely to be fixed anytime soon after talking with the libcurl maintainer.
See all of the examples under the examples/
directory. Below are some simple examples
to get your started on using liblifthttp with both the synchronous and asynchronous APIs.
#include <iostream>
#include <lift/lift.hpp>
int main()
{
const std::string url{"http://www.example.com"};
// Every HTTP request in this example has a 10 second timeout to complete.
const std::chrono::seconds timeout{10};
// Synchronous requests can be created on the stack.
lift::request sync_request{url, timeout};
// Debug information about any request can be added by including a callback handler for debug
// information. Just pass in a lambda to capture the verbose debug information.
sync_request.debug_info_handler(
[](const lift::request& /*unused*/, lift::debug_info_type type, std::string_view data)
{ std::cout << "sync_request (" << lift::to_string(type) << "): " << data; });
// Set the http method for this synchronous request.
sync_request.method(lift::http::method::post);
// Add headers to this request, they are given as a name + value pair. Note that if the same
// header name is specified more than once then it will appear that many times in the request.
sync_request.header("x-lift", "lift-custom-header-data");
// Add some data to the request body, note that if the request http verb is not post or put then
// this data call will set the http verb to post since the request now includes a body.
sync_request.data("lift-hello-world-data");
// This is the blocking synchronous HTTP call, this thread will wait until the http request
// completes or times out.
auto sync_response = sync_request.perform();
std::cout << "Lift status (sync): " << lift::to_string(sync_response.lift_status()) << "\n";
std::cout << sync_response << "\n\n"; // Will print the raw http response.
// Asynchronous requests must be created on the heap and they also need to be executed through
// a lift::client instance. Creating a lift::client automatically spawns a background event
// loop thread to exceute the http requests it is given. A lift::client also maintains a set
// of http connections and will actively re-use available http connections when possible.
lift::client client{};
// Create an asynchronous request that will be fulfilled by a std::future upon its completion.
auto async_future_request = std::make_unique<lift::request>(url, timeout);
// Create an asynchronous request that will be fulfilled by a callback upon its completion.
// It is important to note that the callback will be executed on the lift::client's background
// event loop thread so it is wise to avoid any heavy CPU usage within this callback otherwise
// other outstanding requests will be blocked from completing.
auto async_callback_request = std::make_unique<lift::request>(url, timeout);
// Starting the asynchronous requests requires the request ownership to be moved to the
// lift::client while it is being processed. Regardless of the on complete method, future or
// callback, the original request object and its response will have their ownership moved back
// to you upon completion. If you hold on to any raw pointers or references to the requests
// while they are being processed be sure not to use them until the requests complete. Modifying
// a request's state during execution is prohibited.
// Start the request that will be completed by future.
auto future = client.start_request(std::move(async_future_request));
// Start the request that will be completed by callback.
client.start_request(
std::move(async_callback_request),
[](lift::request_ptr async_callback_request_returned, lift::response async_callback_response)
{
// This on complete callback will run on the lift::client background event loop thread.
std::cout << "Lift status (async callback): ";
std::cout << lift::to_string(async_callback_response.lift_status()) << "\n";
std::cout << async_callback_response << "\n\n";
});
// Block until the async future request completes, this returns the original request and the response.
// Note that the other callback request could complete and print to stdout before or after the future
// request completes since its lambda callback will be invoked on the lift::client's thread.
auto [async_future_request_returned, async_future_response] = future.get();
std::cout << "Lift status (async future): ";
std::cout << lift::to_string(async_future_response.lift_status()) << "\n";
std::cout << async_future_response << "\n\n";
// The lift::client destructor will block until all outstanding requests complete or timeout.
return 0;
}
C++17 compilers tested
g++-9
g++-11
g++-13
clang-9
clang-14
CMake
make or ninja
pthreads
libcurl-devel >= 7.59
*UNSUPPORTED* 7.81.0 has a known libcurl mutli* bug that was fixed in 7.82.0.
libuv-devel
zlib-devel
openssl-devel (or equivalent curl support ssl library)
stdc++fs
Tested on:
ubuntu:20.04
ubuntu:22.04 (with custom libcurl built)
ubuntu:24.04
fedora:31
# This will produce a static library to link against your project.
mkdir Release && cd Release
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
CMake options:
Name | Default | Description |
---|---|---|
LIFT_BUILD_EXAMPLES | ON | Should the examples be built? |
LIFT_BUILD_TESTS | ON | Should the tests be built? |
LIFT_CODE_COVERAGE | OFF | Should code coverage be enabled? |
LIFT_USER_LINK_LIBRARIES | curl z uv pthread dl stdc++fs | Override lift's target link libraries. |
Note on LIFT_USER_LINK_LIBRARIES
, if override the value then all of the default link libraries/targets must be
accounted for in the override. E.g. if you are building with a custom curl target but defaults for everything else
then -DLIFT_USER_LINK_LIBRARIES="custom_curl_target;z;uv;pthread;dl;stdc++fs"
would be the correct setting.
To use within your cmake project you can clone the project or use git submodules and then add_subdirectory
in the parent project's CMakeList.txt
,
assuming the lift code is in a liblifthttp/
subdirectory of the parent project:
add_subdirectory(liblifthttp)
To link to the <project_name>
then use the following:
add_executable(<project_name> main.cpp)
target_link_libraries(<project_name> PUBLIC lifthttp)
Include lift in the project's code by simply including #include <lift/lift.hpp>
as needed.
CMake can also include the project directly via a FetchContent
declaration. In your project's CMakeLists.txt
include the following code to download the git repository and make it available to link to.
cmake_minimum_required(VERSION 3.11)
# ... cmake project stuff ...
include(FetchContent)
FetchContent_Declare(
lifthttp
GIT_REPOSITORY https://github.com/jbaldwin/liblifthttp.git
GIT_TAG <TAG_OR_GIT_HASH>
)
FetchContent_MakeAvailable(lifthttp)
# ... cmake project more stuff ...
target_link_libraries(${PROJECT_NAME} PUBLIC lifthttp)
The tests are automatically run by GitHub Actions on all Pull Requests. They can also be ran locally with a default
localhost instance of nginx
and haproxy
. To do so the CMake option LIFT_LOCALHOST_TESTS=ON
must be set otherwise the tests
will use the hostname nginx
setup in the CI settings. After building and starting nginx
and haproxy
tests can be run by issuing:
# Invoke via cmake:
ctest -v
# Or invoke directly to see error messages if tests are failing:
./test/liblifthttp_tests
Note:
nginx
should be default install/configuration running on port80
.haproxy
should be running on port*3128
with a backend pointing at thenginx
instance. Seedocker/build/haproxy/haproxy.cfg
to update the local configuration.
Using the example benchmark code and a local nginx
instance serving its default welcome page. All benchmarks use keep-alive
connections. The benchmark is compared against wrk
as that is basically optimal performance since
wrk
does zero parsing of the response whereas lift
does.
Here is the CPU the benchmarks were run on:
cat /proc/cpuinfo
...
Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
Here is how the benchmark application is called:
$ ./examples/lift_benchmark --help
Usage: ./examples/lift_benchmark<options> <url>
-c --connections HTTP Connections to use per thread.
-t --threads Number of threads to use.
evenly between each worker thread.
-d --duration Duration of the test in seconds
-h --help Print this help usage.
Using nginx
as the webserver with the default fedora
configuration.
Connections | Threads | wrk Req/Sec | lift Req/Sec |
---|---|---|---|
1 | 1 | 31,138 | 20,785 |
100 | 1 | 148,121 | 44,393 |
100 | 2 | 220,670 | 81,335 |
100 | 3 | 241,839 | 104,747 |
100 | 4 | 275,633 | 123,481 |
100 | 8 | 249,845 | 143,911 |
File bug reports, feature requests and questions using GitHub Issues
Copyright © 2017-2024, Josh Baldwin