Using HTTP in Modern C++

A detailed and practical tutorial for working with HTTP in modern C++ using the cpr library.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In this lesson, and the next few lessons, we’ll focus on letting our programs communicate with other machines, over the internet or other networks. This unlocks many new possibilities for us. For example:

  • Programs that load up-to-date data from the internet
  • Multiple-user experiences, such as multiplayer games
  • Live services, where we can do things like store data and save states to "the cloud" (ie, online servers we control). This makes them accessible from other machines, allowing users to collaborate

There are many different ways we can use to accomplish this - the options we have are often referred to as networking protocols. Complex applications often use multiple protocols, depending on their use case. Useful protocols broadly fall into two categories:

  • TCP-based protocols, which are reliable but slow
  • UDP-based protocols, which are fast but unreliable

HTTP (hypertext transfer protocol) is a common choice within the TCP category. Later in this chapter, we’ll look at protocols in the second category, which we rely on when making fast-paced, real-time applications such as multiplayer games.

We’re likely most familiar with HTTP, and its secure, encrypted extension HTTPS, in the context of browsing the web. It’s how our browsers request and receive web pages, images, and other data. We’ve likely seen the https:// prefix to URLs in our address bar.

But HTTP has become ubiquitous in other contexts, too. Countless organizations are offering their data and products via HTTP, often for free. Additionally, if we ever need to build our own HTTP services to support our products, there are hundreds of tools that make that quick and easy.

Using HTTP in C++ with cpr

Many C++ libraries allow us to make use of HTTP within our programs. In this lesson, we’ll use cpr

CPR can be installed through our package manager. In a previous lesson, we walked through the installation of vcpkg:

If we were using vcpkg as our package manager, we would use it to install cpr from our terminal in the same way we install any other vcpkg package:

.\vcpkg install cpr

Regardless of the installation process we use, we should ensure cpr is set up correctly in our project before proceeding.

We can do that by ensuring the preprocessor can find the header files, and then creating an object with a type like cpr::Url to ensure the linker can find the library:

#include <cpr/cpr.h>

int main() {
  cpr::Url Test;
}

If this compiles, we should be all set!

HTTP APIs

There are thousands of public HTTP APIs we can use. A list of free options is available here: https://github.com/public-apis/public-apis

For our examples in this lesson, we’ll use the Dungeons and Dragons API, available here: http://www.dnd5eapi.co/

Feel free to experiment with any but, to follow along with the initial examples, use one that doesn’t require authentication. We’ll cover authentication a little later in this lesson.

The vast majority of these APIs send and receive data in JSON format. So, we’ll be relying on the nlohmann::json library to help us with this. We have a dedicated lesson on JSON, and this library, available here:

Sending an HTTP Request

The common terminology for HTTP traffic is request and response. A computer on the network sends a request to a network location, and a computer at that location sends a response back. The requestor is sometimes called the client, whilst the responder is the server. In this lesson, we focus on being the client - ie, sending requests that some other computer is responding to.

To make an HTTP request using cpr, we need the API URL to which we’ll be sending the request. URLs are stored in the cpr::Url type, which we can create by passing a simple string to the constructor:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{
    "http://www.dnd5eapi.co/api/monsters/"
    "giant-spider"
  };
}

String Code Layout

When defining a string in C++, we can add additional double quotes within the string to split the definition up.

For example, the string "Hello" could be defined as follows:

std::string Greeting{"Hell"    "o"};

This is done for code layout purposes - typically to define a large string across multiple lines. We’ll use this technique extensively throughout this lesson, particularly when working with URLs:

cpr::Url SomeUrl{
  "http://www.some-website.com/"
  "some-directory/within-the-site"
};

To use this URL to make an HTTP request, we can pass it to the cpr::Get function. This returns a cpr::Response object, containing the server’s response:

#include <cpr/cpr.h>
#include <iostream>

int main(){
  cpr::Url URL{
    "http://www.dnd5eapi.co/api/"
    "monsters/giant-spider"};

  cpr::Response Response{cpr::Get(URL)}; 

  std::cout << Response.text;
}

A cpr::Response includes a text field, containing the body of the response. The previous code outputs a large blob of JSON, with 30+ fields covering abilities, stats, equipment, resistances, and more.

We’ve formatted and shown a small sample of the response below:

{
  "name": "Giant Spider",
  "desc": "To snare its prey, a giant spider...",
  "type": "beast",
  "hit_points": 26,
  "xp": 200,
  "special_abilities": [
    ...
  ],
  ...
}

To know what responses we can expect from an API, we can check the documentation for the specific service we’re using. Monster records of the D&D5 API include fields like name and desc.

We can use our JSON library to parse the data and convert these fields to regular C++ types which we can work with normally. Below, we grab the name and description of the monster as std::string objects:

#include <cpr/cpr.h>
#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main() {
  cpr::Url URL{
    "http://www.dnd5eapi.co/api/"
    "monsters/giant-spider"
  };

  cpr::Response Res{cpr::Get(URL)};

  json Doc{json::parse(Res.text)};

  std::cout
      << Doc.at("name").get<std::string>()
      << '\n'
      << Doc.at("desc").get<std::string>();
}
Giant Spider
To snare its prey, a giant spider spins
elaborate webs or shoots sticky strands of
webbing from its abdomen. Giant spiders are 
most commonly found underground, making their
lairs on ceilings or in dark, web-filled
crevices. Such lairs are often festooned 
with web cocoons holding past victims.

Response Headers

An HTTP response has two different components:

  • A body, which contains the main content of the response. This is available through the text field of a cpr::Response, as we saw above.
  • A collection of headers, which provide supplemental metadata. This is available through the header field of a cpr::Response

Headers are a collection of key-value pairs, containing information like the date the response was generated, how long it is, the type of data that is in the body, and more.

cpr implements each header as a std::pair where the key is first and the value is second.

These pairs are then gathered together into an associative container with an API similar to a std::map. This container is accessible through the header variable on a cpr::Response object.

We can access individual headers using the [] operator, or iterate through the headers using techniques such as a range-based for loop:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{
      "http://www.dnd5eapi.co/api/monsters/"
      "giant-spider"};

  cpr::Response Response = cpr::Get(URL);

  auto t = Response.header["date"];

  std::cout << "The date header is "
            << Response.header["date"];

  std::cout << "\n\nAll Headers:\n";

  for (auto& Header : Response.header) {
    std::cout << Header.first << ": "
              << Header.second << '\n';
  }
}
The date header is Tue, 27 Jun 2023 20:11:10 GMT

All Headers:
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 2558
Content-Type: application/json; charset=utf-8
Date: Tue, 27 Jun 2023 20:11:10 GMT
Etag: W/"9fe-cSLNtEpeu+jMBqWZ25Erl2gQZz8"
Server: Cowboy
Via: 1.1 vegur
X-Powered-By: Express
X-Ratelimit-Limit: 10000
X-Ratelimit-Remaining: 9999
X-Ratelimit-Reset: 1687896672

The HTTP specification includes some standard header names, like Date and Content-Type which servers can choose to include. Servers can additionally include their own, custom headers, that might be useful to consumers of their specific service.

By convention, these custom headers start with X-. In the above example, we see the API is returning some rate-limiting information as custom headers.

These headers are telling us we’re limited to 10,000 requests, we have 9,999 remaining, and our allocation will be reset at 1687896672, which is a date and time represented as a Unix timestamp

Unix Time

"Unix time" is a commonly used convention when computer systems are communicating dates and times with each other.

A Unix timestamp treats any point in time as a single integer. That integer represents the number of seconds between the chosen date, and 00:00:00 UTC on 1st January 1970, sometimes referred to as the Unix epoch

Date/time libraries, such as std::chrono have utilities to handle unix timestamps, and websites such as unixtimestamp.com allow us to convert human-readable dates to Unix timestamps, and vice versa.

HTTP Status Codes

So far, we’ve been assuming our HTTP request works as expected. That is not always the case. Networking is never 100% reliable.

HTTP responses include a status code, which summarises the result of the request.

The numeric status code is available through the status_code property of cpr::Response objects. A string is available as status_line which provides some additional info:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL1{
      "http://www.dnd5eapi.co/api/monsters/"
      "giant-spider"};

  cpr::Response Response1{cpr::Get(URL1)};

  std::cout << "Response1 Status Code: "
            << Response1.status_code;
  std::cout << "\nResponse1 Status Line: "
            << Response1.status_line;

  cpr::Url URL2{
      "http://www.dnd5eapi.co/api/monsters/"
      "does-not-exist"};

  cpr::Response Response2{cpr::Get(URL2)};

  std::cout << "\n\nResponse2 Status Code: "
            << Response2.status_code;
  std::cout << "\nResponse2 Status Line: "
            << Response2.status_line;
}
Response1 Status Code: 200
Response1 Status Line: HTTP/1.1 200 OK

Response2 Status Code: 404
Response2 Status Line: HTTP/1.1 404 Not Found

A list of all possible status codes is available on this Wikipedia page. We’ve summarised the most common response codes below, and how they apply to our use cases

200-299 range: success

Successful responses trigger 2xx status codes. The most common is 200

300-399 range: redirection

When service providers move their resources to a different URL, it’s common to leave a breadcrumb at the old location, to tell consumers it has moved. When this happens, the status code should be in the 3xx range.

The new location is provided as a header within the HTTP response. The header will have the key of location

By default, when cpr encounters a redirection response, it will automatically "follow" the redirect. That is, it will make a new request to the URL specified in the location header.

So, unless we change that behavior, we won’t see a 3xx response from cpr. The official docs show how we can modify the redirect behavior.

400-499 range: client errors

Errors in the 4xx range often indicate there are problems with the request we made. These are commonly called "client errors", as they’re things that we as the client can fix. The common status codes we’ll encounter in this category include:

  • 401 - the action we’re trying to perform requires us to authenticate ourselves. We cover authentication a little later in this section
  • 403 - we do not have permission to do what we’re trying to do
  • 404 - the URL we’re making a request to cannot be found
  • 422 - the contents of our HTTP request are not valid. This is most common when we’re trying to create or edit a record on the server, which we’ll cover later in this section
  • 429 - we’ve made too many requests to the API. APIs generally limit the number of requests users can make. This can be part of the API’s business model - eg, the service is free for light use, but heavy users may need to pay

500-599 range: server errors

Errors in the 5xx range generally indicate something has gone wrong on the service provider's side. The API may be down or have some defect that our request is triggering.

Request Parameters

Aside from the URL, our HTTP requests will often need to include additional data. The providers of the API will have documented what data we need to provide, in what situation, and in what way.

There are three ways we can provide additional data as part of an HTTP request:

  • URL parameters
  • The request headers
  • The request body

We’ll cover all of them here, starting with URL parameters.

URL parameters are generally the most visible of these, as they show up in the URL. As such, we may have already noticed them in our browser’s address bar as we navigate the internet.

For example, a Google search for C++ generates the following URL:

https://www.google.com/search?q=C++

This URL has a parameter called q, with a value of C++

The part of the URL that contains the parameters is sometimes called the query string. It begins with a ? and is then followed by a collection of key-value pairs. Keys and values are separated by =, whilst parameters are separated by &.

If we wanted to provide the following three parameters:

cat: meow
dog: bark
cow: moo

Our query string would look like this:

?cat=meow&dog=bark&cow=moo

We could attempt to build these strings and add them to our URL. However, most HTTP libraries provide a more convenient abstraction. In cpr, we have the cpr::Parameters type that stores a collection of key-value pairs. We can create them like this:

cpr::Parameters Params{{"cat", "meow"},
                       {"dog", "bark"}};

Once created, we can just pass our parameters into our Get calls as an argument:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{"http://www.example.com"};
  cpr::Parameters Params{{"cat", "meow"},   
                         {"dog", "bark"}};  

  cpr::Response Response{cpr::Get(URL, Params)};  

  // The url property contains the URL
  // that was used for the request
  std::cout << "URL: " << Response.url;
}
URL: http://www.example.com/?cat=meow&dog=bark

cpr::Get Arguments

Methods like cpr::Get are set up to accept arguments in any order. It infers how to use arguments based on the argument type, rather than the position within the argument list. For example, the URL will be taken from the cpr::Url object, regardless of where it appears in the argument list:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{"http://www.example.com"};
  cpr::Parameters Params{{"cat", "meow"}};

  cpr::Response Res1{cpr::Get(URL, Params)};
  cpr::Response Res2{cpr::Get(Params, URL)};

  if (Res1.url == Res2.url) {
    std::cout << "Equivalent!";
  }
}
Equivalent!

When we introduce other HTTP methods and asynchronous requests later in this lesson, the way arguments are handled is the same as what we showed here. We can pass just the arguments we need, and we can do it in any order.

Request Headers

Just like responses can contain headers, so too can our requests. What headers (if any) we need will depend on the API we’re using. For example, some APIs require us to pass authentication information, or API keys in the headers of our requests.

In cpr, we can create a collection of headers using the cpr::Header type, and pass them into our cpr::Get request as an argument.

We can use a simple utility API at http://httpbin.org/headers that will echo back the HTTP headers we used, as a JSON response.

In our code, it looks like this:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{
      "http://www.httpbin.org/headers"};
  cpr::Header Headers{{"cat", "meow"},
                      {"dog", "bark"}};

  cpr::Response Response{
      cpr::Get(URL, Headers)};

  std::cout << Response.text;
}

We can see our headers were added to the request. Our machine, and intermediate servers our request passes through, may also add some additional headers:

{
  "headers": {
    "Accept": "*/*",
    "Cat": "meow",
    "Dog": "bark",
    "Host": "www.httpbin.org",
    "User-Agent": "curl/8.1.2-DEV",
    "X-Amzn-Trace-Id": "redacted"
  }
}

HTTP Methods and Request Bodies

So far, our HTTP requests have been attempting to retrieve data from the server. These are called GET requests, which we’ve been calling with cpr::Get. "GET" is an example of an HTTP method, and we have many more we can use.

As always, it’s up to the service provider to decide which HTTP methods they support on each URL, as well as how those methods work, and who can use them. But, in general, the following conventions tend to be followed:

  • GET (cpr::Get) requests are for retrieving records
  • POST (cpr::Post) requests are for creating records
  • DELETE (cpr::Delete) requests are for deleting records
  • PATCH (cpr::Patch) requests are for editing properties within records
  • PUT (cpr::Put) requests are for replacing entire records

With many of these methods, it’s generally anticipated that we provide some data in the body of our request. For example, if we want to create a record, we need to provide the fields for that record.

With cpr, we can create a body using the cpr::Body type, and include it as an argument when making our request with a function like cpr::Post:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{"https://www.example.com/post"};
  cpr::Body Body{"Hello World"};
  cpr::Response Response{cpr::Post(URL, Body)};
}

We can send any type of data over HTTP, such as JSON, images, or videos. Therefore, when providing a body, it’s also generally recommended to provide a Content-Type header, describing what the content in the body represents.

Content types have a category, like "image" and a subtype, like "jpeg". The type and subtype are separated by a /. Some examples include:

  • image/jpeg
  • text/plain
  • video/mp4
  • application/json

When communicating with JSON APIs, the content type is generally going to be application/json. The https://www.httpbin.org/post API gives us a quick way to check the contents of our request body

#include <cpr/cpr.h>
#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main() {
  using namespace nlohmann::literals;

  json Doc{R"({
    "name": "Bob",
    "age": 30
  })"_json};

  cpr::Url URL{"https://www.httpbin.org/post"};
  cpr::Header Headers{
      {"Content-Type", "application/json"}};
  cpr::Body Body{Doc.dump()};

  cpr::Response Res =
      cpr::Post(URL, Headers, Body);

  std::cout << Res.text;
}
{
  "args": {},
  "data": "{\"name\":\"Bob\",\"age\":30}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Content-Length": "52",
    "Content-Type": "application/json",
    "Host": "www.httpbin.org",
    "User-Agent": "curl/8.1.2-DEV",
    "X-Amzn-Trace-Id": "redacted"
  },
  "json": {
    "name": "Bob",
    "password": "secret"
  },
  "origin": "redacted",
  "url": "<https://www.httpbin.org/post>"
}

Authentication

Often, when we’re making HTTP calls, we need to provide authentication details. With APIs, the service will often provide us with a secret API key, which we need to attach to all of our HTTP requests.

This is typically done as a request header or request parameter, depending on the API. We covered those earlier in the lesson so, if this is how the API you’re using chooses to authenticate, we should be all set.

However, HTTP also has its own, native form of authentication, managed through a special Authentication header.

There are three common ways of doing HTTP authentication:

  • Basic Authentication
  • Digest Authentication
  • Bearer Tokens

The method the server requires us to use will determine the value we pass in that authentication header.

Fortunately, cpr can help us manage the complexity here. We just need to pass an appropriate cpr::Authentication object along as an argument.

Basic Authentication

Basic HTTP authentication uses a simple username and password combination, which is placed into the Authentication header

Below, we use the http://www.httpbin.org/basic-auth/ API to simulate a URL that requires basic authentication.

We include the username and password we want to use in the URL - for example, opening http://www.httpbin.org/basic-auth/bob/secret in a web browser will prompt us for a username and password. The correct username will be bob, and the correct password will be secret.

When accessing the URL programmatically, we need to construct an appropriate Authentication header that contains the username and password, which cpr can construct for us:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{
      "http://www.httpbin.org/basic-auth/"
      "bob/secret"};

  cpr::Authentication Auth{
      "bob", "secret", cpr::AuthMode::BASIC};

  cpr::Response Response{cpr::Get(URL, Auth)};

  std::cout << Response.text;
}
{
  "authenticated": true,
  "user": "bob"
}

Basic Authentication and Encryption

On the internet, our requests often need to travel through a dozen or more devices before they reach their destination. Any of those devices can see our requests, including the body and headers.

Basic authentication puts our username or password in an HTTP header, without encryption. This makes our username and password visible to any of those devices. Because of this, when using basic authentication, we should ensure our requests are encrypted in some other way.

Typically, this means ensuring they are sent over https, which encrypts everything in our request aside from the destination address

Digest Authentication

Digest authentication is similar to basic authentication in the sense that it is based on a username and password. However, the credentials are inserted into the Authentication header in an encrypted form.

Ensuring the credentials are encrypted in a way the server can decrypt is quite a complex process, involving additional preliminary communication between our client and the server before we even send our main request.

However, cpr manages all that complexity for us. We just need to create a cpr::Authentication object containing our username, password, and the cpr::AuthMode::DIGEST flag.

Similar to the previous example, httpbin.org provides a service where we can test our digest authentication. http://www.httpbin.org/digest-auth/auth/bob/secret will ask us to provide a username of bob, and a password of secret.

It will use the digest authentication process to receive and validate our credentials.

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{
      "http://www.httpbin.org/digest-auth/auth/"
      "bob/secret"};

  cpr::Authentication Auth{
      "bob", "secret", cpr::AuthMode::DIGEST};

  cpr::Response Response{cpr::Get(URL, Auth)};

  std::cout << Response.text;
}
{
  "authenticated": true,
  "user": "bob"
}

Bearer Token

The final form of authentication commonly used involves a token rather than a username and password. Services that use this process will provide us with a token - a long string that we keep secret.

We include the token in the header of our requests and, once the server checks the header and sees our token, it will authenticate the request as having been sent by us, the "bearer" of the token.

We pass the token by setting the Authentication header to the value of Bearer followed by a space, and then the token:

cpr::Header Headers{{"Authentication",
                     "Bearer my-secret-token"}};

Alternatively, we can ensure this header is included in the correct format by creating a cpr::Bearer object with our token and passing it as an argument to our function call.

The http://www.httpbin.org/bearer API lets us make sure we’re passing the token correctly:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{"http://www.httpbin.org/bearer"};
  cpr::Bearer Token{"my-secret-token"};
  cpr::Response Response{cpr::Get(URL, Token)};
  std::cout << Response.text;
}
{
  "authenticated": true,
  "token": "my-secret-token"
}

Asynchronous Requests and Callbacks

The previous code snippets all create what is known as synchronous requests. These have the benefit of being quite simple to use and understand. In essence, it allows us to treat our HTTP communication as a regular function call. We make the request and, on the next line, the response is available to us.

However, there is a problem here - HTTP requests, and network traffic in general, take a relatively long time to complete.

With a synchronous request, our thread is effectively frozen until that process is complete. For this reason, synchronous calls are sometimes also referred to as blocking calls.

A typical HTTP call can take in the region of 50-500 milliseconds to complete. In real-time applications, we can’t be blocked for that long - we need to be responding to user input almost immediately, and updating the screen every 5-30 milliseconds.

For this reason, we have asynchronous requests. Here, we send the request, but we do not wait for it to complete - we keep running the application as normal.

Later, when the request is complete, a function is called to let us react accordingly. This type of function is often referred to as a callback.

In cpr, asynchronous requests that use a callback have names like cpr::GetCallback, cpr::PostCallback, and so on.

Their first argument is a callback function, that will receive the cpr::Response as a parameter. We can choose to receive it as a copy, or as a const reference.

The remaining arguments match the synchronous versions of the respective functions, allowing us to pass URLs, headers, etc in the normal way:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Url URL{
      "http://www.dnd5eapi.co/api/monsters/"
      "giant-spider"};

  auto callback{[](const cpr::Response& Res) {
    std::cout << "Request Complete: "
              << Res.status_code;
  }};

  std::cout << "Starting Request\n";
  cpr::GetCallback(callback, URL);

  // This is not blocked
  std::cout << "Loading...\n";
}
Starting Request
Loading...

This simple program compiles and runs successfully, but it ends before our request is completed.

We can expand it to something slightly more elaborate. Below, we’re using a while loop to keep our application running until something tells it to stop.

This is a common pattern, often called the application loop.

In the following example, our program continues until our HTTP request has been completed. This calls our callback lambda, which sets run to false, thereby causing our while loop to stop and our program to end:

#include <cpr/cpr.h>
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;

int main() {
  bool run{true};
  cpr::Url URL{
      "http://www.dnd5eapi.co/api/monsters/"
      "giant-spider"};

  auto callback{
      [&run](const cpr::Response& Res) {
        json Doc{json::parse(Res.text)};

        std::cout
            << Doc.at("name").get<std::string>()
            << " is ready after " << Res.elapsed
            << " seconds\n";

        run = false;
      }};

  std::cout << "Loading...\n";
  cpr::GetCallback(callback, URL);
  std::cout << "We're not blocked!\n";

  while (run) {
    // App loop
  }
  std::cout << "Bye!";
}
Starting Request
Loading...
Giant Spider is ready after 0.270608 seconds
Bye!

Asynchronous Race Conditions

A common source of confusion and bugs occurs when dealing with asynchronous code. Inherently, we don’t know how long an asynchronous process will take to complete, and the duration can be different each time. For example, we do not know what the output of the following program will be:

#include <cpr/cpr.h>
#include <iostream>

int main() {
  bool run{true};
  cpr::Url URL{"http://www.example.com/"};

  cpr::GetCallback(
      [&run](const cpr::Response& Res) {
        std::cout << "a";
        run = false;
      },
      URL);

  cpr::GetCallback(
      [&run](const cpr::Response& Res) {
        std::cout << "b";
        run = false;
      },
      URL);

  while (run) {
    // app loop
  }
}

It might log a, b, ab, or ba. It depends on which request finishes first, and how much slower the other one is.

This is an example of a race condition, and they happen any time we have asynchronous processes running concurrently, and our code is making assumptions about which one will finish first. If our assumption is wrong, our program will not behave as expected.

These bugs can be particularly insidious, as they can be very hard to detect. For example, if the process we’re assuming will win the race does indeed win 99% of the time, we may never detect we have a race condition. But, once our program is deployed at scale, the 1% edge case will happen regularly.

We cover concurrency and parallel programming in more detail in a dedicated chapter later in the course.

Sessions

cpr provides an alternative way to manage our HTTP requests, in the form of session objects. Unlike the functions we’ve been using before, sessions are stateful. We can provide them with values that are maintained, even after we send a request, and receive a response.

This is particularly useful when we want to send multiple requests with the same, or similar, settings. We can set our session up with URLs, parameters, headers, callbacks, and interceptors (covered next), and then re-use them to make multiple calls through the life of our application.

We update the state of our sessions using functions like SetUrl() and SetParameters(). When we’re ready to make a request, we use functions like Get(), Post(), and Patch() depending on the HTTP method we want to use,

#include <cpr/cpr.h>
#include <iostream>

int main() {
  cpr::Session Session;
  Session.SetUrl("http://www.test.com");

  Session.SetParameters(
      {{"first", "one"}, {"second", "two"}});

  cpr::Response Res1{Session.Get()};
  std::cout << "Completed request: \n"
            << Res1.url;

  Session.SetParameters({{"third", "three"}});
  cpr::Response Res2{Session.Get()};
  std::cout << "\nCompleted request: \n"
            << Res2.url;
}
Completed request:
http://www.test.com/?first=one&second=two
Completed request:
http://www.test.com/?third=three

Sessions can also be used with asynchronous requests, using methods like GetCallback() and PostCallback(). However, when doing this, we need to manage the session through a shared pointer. This allows cpr to maintain its lifecycle correctly.

#include <cpr/cpr.h>
#include <iostream>
#include <memory>

int main() {
  bool run{true};
  auto Session{
      std::make_shared<cpr::Session>()};

  Session->SetUrl("http://www.example.com");

  Session->GetCallback([&run](cpr::Response R) {
    std::cout << "Request Complete!";
    run = false;
  });

  std::cout << "Loading...\n";

  while (run) {
    // App loop
  }
}
Loading...
Request Complete!

Interceptors (Middleware)

When working with a session, we can provide an interceptor to that session. Interceptors are functions that allow us to run code before our request is sent over the network, or before our response is returned to the caller.

This design pattern is sometimes also referred to as middleware.

Interceptors allow us to modify or monitor the traffic flowing through the session.

A minimal example of an interceptor looks like this:

#include <cpr/cpr.h>
using cpr::Session, cpr::Response;

class MyInterceptor : public cpr::Interceptor {
 public:
  Response intercept(Session& s) override {
    Response Res{proceed(s)};
    return Res;
  }
};

To summarise the key points:

  • Interceptors inherit from the cpr::Interceptor class
  • They override the intercept method.
  • The intercept method receives a reference to the cpr::Session and returns the cpr::Response to the caller.
  • We can get the cpr::Response by calling the inherited proceed method and passing in the session.

This interceptor doesn’t do anything, but it outlines the basic framework. We can now add code to this scaffolding as needed. For example, we can monitor or modify the session or the cpr::Response objects.

Things we want to happen before the request is sent need to appear before the call to proceed(), and things we want to happen after the response is received should come after it.

Finally, we attach an interceptor to a session by passing it a shared pointer to the AddInterceptor method.

Below, we’ve added an interceptor that simply logs some information about the traffic flowing through the session to which it is attached:

#include <cpr/cpr.h>
#include <iostream>
#include <memory>
using cpr::Session, cpr::Response;

class MyInterceptor : public cpr::Interceptor {
 public:
  Response intercept(Session& Ses) override {
    std::cout << "Request to "
              << Ses.GetFullRequestUrl()
              << " started";

    Response Res{proceed(Ses)};

    std::cout << "\nResponse received after "
              << Res.elapsed << " seconds";

    return Res;
  }
};

int main() {
  Session Ses{};
  Ses.AddInterceptor(
      std::make_shared<MyInterceptor>());
  Ses.SetUrl("http://www.example.com");
  Ses.Get();
}
Request to http://www.example.com started
Response received after 0.196316 seconds

We can trigger multiple HTTP requests from a single call to intercept. This can be done by calling proceed multiple times or using any other way of creating HTTP requests we’ve seen.

This is useful for scenarios where we want our interceptor to retry failed requests, or we need to make preliminary or follow-up requests.

In the following example, we create an interceptor that will retry requests if the server responds with an error. We’re sending requests to http://www.httpbin.org/status/504 - an HTTP utility API that will respond with the status code we specify.

In this case, we’re asking it to return a 504 error, which is the response we’d get if the server is taking too long to respond.

We’ve added middleware that retries failing requests up to three times. This is a realistic use case - networking can be inherently unstable and requests can fail at random. When this happens, we often want to try again rather than immediately give up:

#include <cpr/cpr.h>
#include <iostream>
#include <memory>

using cpr::Session, cpr::Response;
class MyInterceptor : public cpr::Interceptor {
 public:
  Response intercept(Session& Ses) override {
    std::cout << "Request to "
              << Ses.GetFullRequestUrl();

    Response Res;
    int RemainingTries{3};
    while (RemainingTries > 0) {
      Res = proceed(Ses);
      if (Res.status_code >= 500) {
        std::cout << "\nRequest failed: "
                  << Res.status_line;
        --RemainingTries;
      } else {
        return Res;
      }
    }

    std::cout << "\nNo retries left - "
                 "mission failed";

    return Res;
  }
};

int main() {
  Session Ses{};
  Ses.AddInterceptor(
      std::make_shared<MyInterceptor>());
  Ses.SetUrl(
      "http://www.httpbin.org/status/504");
  Ses.Get();
}
Request to http://www.httpbin.org/status/504
Request failed: HTTP/1.1 504 GATEWAY TIMEOUT
Request failed: HTTP/1.1 504 GATEWAY TIMEOUT
Request failed: HTTP/1.1 504 GATEWAY TIMEOUT
No retries left - mission failed

We can also have multiple interceptors attached to a single session. In the following scenario, the "B" interceptor is added to our session after the "A" interceptor:

#include <cpr/cpr.h>
#include <iostream>
#include <memory>
using cpr::Session, cpr::Response;

class AInterceptor : public cpr::Interceptor {
 public:
  Response intercept(Session& Ses) override {
    std::cout << "A Starting\n";
    Response Res{proceed(Ses)};
    std::cout << "A Done\n";
    return Res;
  }
};

class BInterceptor : public cpr::Interceptor {
 public:
  Response intercept(Session& Ses) override {
    std::cout << "B Starting\n";
    Response Res{proceed(Ses)};
    std::cout << "B Done\n";
    return Res;
  }
};

int main() {
  Session Ses{};
  Ses.AddInterceptor(
      std::make_shared<AInterceptor>());
  Ses.AddInterceptor(
      std::make_shared<BInterceptor>());
  Ses.SetUrl(
      "http://www.httpbin.org/status/200");
  Ses.Get();
}
A Starting
B Starting
B Done
A Done

We can see later interceptors being somewhat "nested" inside the ones that came before.

Specifically, the proceed call from A is calling the next interceptor, B. There are no interceptors after B, so B’s proceed call is making the HTTP request. When B receives the response it returns it to A to pick up where it left off.

This is perhaps not surprising, as we’re just dealing with a function call stack here. The only difference is that we don’t need to determine what the next function is - we just always call proceed(). Behind the scenes, cpr is managing the process for us, deciding what the next interceptor should be or, if there are no more interceptors, proceeding to start the HTTP request.

Summary: Using HTTP in C++

In this lesson, we explored various aspects of using HTTP in C++, to enable communication between programs and external resources over the Internet. Key topics covered include:

  • Networking Protocols: Introduction to networking protocols, with a focus on HTTP (Hypertext Transfer Protocol) and its significance in web-based communications.
  • Using the cpr Library: Installing the cpr library, and using it to make HTTP requests and responses easier to create and manage
  • HTTP APIs: How to interact with public HTTP APIs, with an example using the Dungeons and Dragons API.
  • HTTP Request and Response: Detailed discussion on the structure and processing of HTTP requests and responses, including URL handling and parsing JSON responses.
  • HTTP Methods and Status Codes: Overview of different HTTP methods like GET, POST, DELETE, etc., and understanding HTTP status codes and their implications.
  • Handling Headers and Parameters: Techniques for managing headers in HTTP requests and responses, and incorporating parameters (query strings) into HTTP requests.
  • Authentication Methods: Understanding different authentication mechanisms in HTTP, including Basic, Digest, and Bearer Token authentication, and how to use them with cpr.
  • Asynchronous Requests: Introduction to asynchronous HTTP requests in C++, explaining the concept and implementation of non-blocking calls with callbacks.
  • Handling Sessions and Interceptors: Utilizing sessions for stateful HTTP communication and interceptors (middleware) for modifying or monitoring HTTP traffic.

This lesson provides a foundational understanding of HTTP communication in C++ and equips learners with the skills to integrate network capabilities into their C++ applications.

Was this lesson useful?

Next Lesson

Binary Serialization using Cereal

A detailed and practical tutorial for binary serialization in modern C++ using the cereal library.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Libraries and Dependencies
Next Lesson

Binary Serialization using Cereal

A detailed and practical tutorial for binary serialization in modern C++ using the cereal library.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved