A cross platform C++ HTTP framework.
Developed at Spotify 2019-2022, Discontinued and handed over to new maintainers January 2023
At Spotify we have performed studies that show the efficacy of using native backed solutions for interfacing to backends, especially when it came to the battery life of certain devices. In order to carry this forward in the cross-platform C++ world, we created this library that provides a common interface to many of the system level HTTP interfaces, and predictable caching and request hooking. We found that many of the current solutions that claimed to do this lacked key supports for many kinds of platforms, and ended up being libraries that heavily favoured 1 platform and gave the other platforms a generic implementation. We also wanted to provide a caching layer that was consistent across all platforms in our layered architecture.
NFHTTP
is designed as a common C++ interface to communicate with different systems over HTTP! The API allows you to create objects to make Requests
and read Responses
. To initiate, send and receive messages you create and use a Client
object. This is a layered architecture where requests and responses can pass through multiple places in the stack and get decorated or have actions taken upon them.
The layer design is as follows:
Our support table looks like so:
OS | Underlying Framework | Status |
---|---|---|
iOS | NSURLSession | Stable |
OSX | NSURLSession | Stable |
Linux | curl | Stable |
Android | curl | Beta |
Windows | WinHTTP | Alpha |
In addition to this, it is also possible to use curl on any of the above platforms or boost ASIO (provided by CPP REST SDK).
NFHTTP
is a Cmake project, while you are free to download the prebuilt static libraries it is recommended to use Cmake to install this project into your wider project. In order to add this into a wider Cmake project (who needs monorepos anyway?), simply add the following lines to your CMakeLists.txt
file:
add_subdirectory(NFHTTP)
# Link NFHTTP to your executables or target libs
target_link_libraries(your_target_lib_or_executable NFHTTP)
Generate an Xcode project from the Cmake project like so:
$ git submodule update --init --recursive
$ mkdir build
$ cd build
$ cmake .. -GXcode
Generate a Ninja project from the Cmake project like so:
$ git submodule update --init --recursive
$ mkdir build
$ cd build
$ cmake .. -GNinja
Use gradle
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.spotify.nfhttptest_android"
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName "1.0"
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_APP=1 -DANDROID=1"
}
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/cpp']
}
}
externalNativeBuild {
cmake {
path "../CMakeLists.txt"
}
}
}
Generate a Visual Studio project from the Cmake project like so:
$ mkdir build
$ cd build
$ cmake .. -G "Visual Studio 12 2013 Win64"
In order to execute HTTP requests, you must first create a client like so:
auto client = nativeformat::http::createClient(nativeformat::http::standardCacheLocation(),
"NFHTTP-" + nativeformat::http::version());
It is wise to only create one client per application instance, in reality you will only need one (unless you need to separate the caching mechanism for your own reasons). After you have done this you can proceed to creating request objects like so:
const std::string url = "http://localhost:6582/world";
auto request = nativeformat::http::createRequest(url, std::unordered_map<std::string, std::string>());
This will create a GET request with no added headers to send to the localhost:682/world location. This does not mean other headers will not be added, we have multiple layers that will add caching requirement headers, language headers, content size headers and the native layer can also add headers as it sees fit. After we have created our request we can then execute it:
auto token = client->performRequest(request, [](const std::shared_ptr<nativeformat::http::Response> &response) {
printf("Received Response: %s\n", response->data());
});
The callback will be called asynchronously in whatever thread the native libraries post the response on, so watch out for thread safety within this callback. In order to execute requests synchronously on whatever thread you happen to be on, you can perform the follow actions:
auto response = client->performSynchronousRequest(request);
printf("Received Response: %s\n", response->data());
You might wonder how you can hook requests and responses, this can be done when creating the client, for example:
auto client = nativeformat::http::createClient(nativeformat::http::standardCacheLocation(),
"NFHTTP-" + nativeformat::http::version(),
[](std::function<void(const std::shared_ptr<nativeformat::http::Request> &request)> callback,
const std::shared_ptr<nativeformat::http::Request> &request) {
printf("Request URL: %s\n", request->url().c_str());
callback(request);
},
[](std::function<void(const std::shared_ptr<nativeformat::http::Response> &response, bool retry)> callback,
const std::shared_ptr<nativeformat::http::Response> &response) {
printf("Response URL: %s\n", response->request()->url().c_str());
callback(response, false);
});
Here we have hooked the client up to receive requests and responses via the hook functions. Because we are now part of the layered architecture, we can perform any changes we want on the requests or responses, such as decorating with OAuth tokens, redirecting to other URLs, retrying responses or even cancelling responses altogether. If you are interested in the concept of cache pinning, it can be done like so:
client->pinResponse(response, "my-offlined-entity-token");
This will then ensure that the response is in the cache until it is explicitly removed, and ignore all backend caching directives.
Contributions are welcomed, have a look at the CONTRIBUTING.md document for more information.
The project is available under the Apache 2.0 license.