nativeformat / NFHTTP

A cross platform C++ HTTP library that interfaces natively to other platforms.
Apache License 2.0
588 stars 37 forks source link
NFHTTP

CircleCI License Spotify FOSS Slack Readme Score

A cross platform C++ HTTP framework.

Developed at Spotify 2019-2022, Discontinued and handed over to new maintainers January 2023

Raison D'être :thought_balloon:

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.

Architecture :triangular_ruler:

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).

Dependencies :globe_with_meridians:

Installation :inbox_tray:

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)

For iOS/OSX

Generate an Xcode project from the Cmake project like so:

$ git submodule update --init --recursive
$ mkdir build
$ cd build
$ cmake .. -GXcode

For linux

Generate a Ninja project from the Cmake project like so:

$ git submodule update --init --recursive
$ mkdir build
$ cd build
$ cmake .. -GNinja

For Android

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"
        }
    }
}

For Windows

Generate a Visual Studio project from the Cmake project like so:

$ mkdir build
$ cd build
$ cmake .. -G "Visual Studio 12 2013 Win64"

Usage example :eyes:

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.

Contributing :mailbox_with_mail:

Contributions are welcomed, have a look at the CONTRIBUTING.md document for more information.

License :memo:

The project is available under the Apache 2.0 license.

Acknowledgements

Contributors