HTTP Clients

Making HTTP Requests

The class Pt::Http::Client can be used to establish a connection to an HTTP server and then send requests and receive replies. Synchronous and asynchronous operation is supported, however the latter is recommended. The following example demonstrates the asynchronous client API:

void onReplyReceived(Pt::Http::Client& client);
int main()
{
Pt::Net::Endpoint ep("www.google.com", 80);
Pt::Http::Client client(loop, ep);
client.request().setUrl("/index.html");
client.replyReceived() += Pt::slot(*this, &onReplyReceived);
client.beginReceive();
loop.run();
}

An event loop is required for asynchronous operation and can be passed to the client's constructor, or set later by calling setActive(). There is no explicit connect method, because connections are established when required i.e. before the request is sent to the server, if the client is not currently connected. The target endpoint can be passed to the constructor, or set using setHost(). The member function request() returns a Pt::Http::Request object, which represents the HTTP request data to be sent to the server, including header and payload. It has to be initialized, before beginReceive() is called, to start the asynchronous HTTP communication. This usually involves setting the resource URI, HTTP method, query parameters or special header fields. The keep-alive header can be set, to request a persistent connection, which can be reused for the next request, if the server permits it. The signal replyReceived() is sent, when reply data is received from the server and ready to be processed. The next example shows the slot that prints the reply data to the standard output:

void onReplyReceived(Pt::Http::Client& client)
{
Pt::Http::Reply& reply = client.reply();
if( progress.header() )
{
std::cout << reply.statusCode() << ' ' << reply.statusText() << std::endl;
}
if( progress.body() )
{
while ( reply.body().rdbuf()->in_avail() )
std::cout << reply.body().get();
}
if( progress.finished() )
{
client.loop()->exit();
return;
}
client.beginReceive();
}

The asynchronous operation is ended by calling endReceive(), which returns a Pt::Http::MessageProgress object. It's member function finished() indicates, whether the reply was completely received. Sending a request and receiving a reply might require more than one operation. If the reply was not finished, beginReceive() has to be called again. It is possible that progress was made, but neither header nor body are available yet. In case of short messages, however, the HTTP header and the complete body are normally received at the same time. In that case, the functions header(), body() and finished() all return true and the operation has completed. If the header or body was received, the member function reply() can be used to get a Pt::Http::Reply object, which represents the received HTTP reply. The body of the HTTP reply can be read from an std::istream, which is obtained by calling body().

Request Pipelining

In order to pipeline HTTP requests, the client has to send multiple requests before receiving the replies. Therefore, the program must not only be notified when the reply was received, but also when the request was send to the server. More requests can then be sent, before starting to receive the replies. The following example demonstrates this:

void onRequestSent(Pt::Http::Client& client);
void onReplyReceived(Pt::Http::Client& client);
int main()
{
Pt::Net::Endpoint ep("animal-pics", 80);
Pt::Http::Client client(loop, ep);
client.request().setUrl("/cat.png");
client.requestSent() += Pt::slot(*this, &onRequestSent);
client.replyReceived() += Pt::slot(*this, &onReplyReceived);
client.beginSend();
loop.run();
}

The client is set up for the initial request and the signal requestSent() is to connected to a slot, which is called when the request progressed. The asynchronous HTTP communication is then started with beginSend(). The next code block shows the code for the slot, which sends another request and then starts receiving the replies:

void onRequestSent(Pt::Http::Client& client)
{
Pt::Http::MessageProgress progress = client.endSend();
if( ! progress.finished() )
{
client.beginSend();
return;
}
if( client.request().url() == "/cat.png" )
{
client.request().setUrl("/dog.png");
client.beginSend();
return;
}
client.beginReceive();
}

The asynchronous operation is ended by calling endSend(), which returns a Pt::Http::MessageProgress object. It is possible that only a part of the request was sent and is not finished yet, in which case beginSend() is called again. If the request for the first resource has completed, the client is initialized to request the second resource asynchronously. At some point, the second request will be completely sent and beginReceive() is called to start receiving the replies. The slot connected to replyReceived() should be prepared to handle two replies, as shown in the next example:

void onReplyReceived(Pt::Http::Client& client)
{
Pt::Http::Reply& reply = client.reply();
if( progress.header() )
{
std::cout << reply.url() << ": " << reply.statusText() << std::endl;
}
if( progress.body() )
{
processImage( reply.body() );
}
if( progress.finished() && reply.url() == "/dog.png")
{
client.loop()->exit();
return;
}
client.beginReceive();
}

The program only exits, when the second reply was finished, otherwise beginReceive() is called repeatedly to advance the asynchronous operations to receive the replies.

Chunked Encoding

To send the request body in chunked-encoding, the client has to be able to continue a send operation with the next chunk of data. The beginSend() method accepts a flag which indicates, if the request is complete or if another chunk needs to be written. The next example shows how a chunked request is started:

void onRequestSent(Pt::Http::Client& client);
void onReplyReceived(Pt::Http::Client& client);
int main()
{
Pt::Net::Endpoint ep("chunks.pool", 80);
Pt::Http::Client client(loop, ep);
client.request().setMethod("POST");
client.request().setUrl("/chunks");
client.request().body() << "CHUNKS:";
client.requestSent() += Pt::slot(*this, &onRequestSent);
client.replyReceived() += Pt::slot(*this, &onReplyReceived);
client.beginSend(false);
loop.run();
}

The client is prepared for a POST request with the first chunk of data in the body of the request. The important detail is that beginSend() is called with the completion flag set to false. When progress has been made, the slot connected to requestSent() will be called, which starts sending the next chunk:

bool hasChunks();
void writeNextChunk(std::ostream& os);
void onChunkedSent(Pt::Http::Client& client)
{
Pt::Http::MessageProgress progress = client.endSend();
if( ! progress.finished() )
{
client.beginSend(false);
return;
}
if( hasChunks() )
{
writeNextChunk( client.request().body() );
client.beginSend(false);
return;
}
client.beginReceive();
}

The Pt::Http::MessageProgress object returned by endSend() only indicates, whether a chunk has finished, not the whole request. Only a part of the chunk might have been sent, in which case beginSend() is called again to finish the chunk. If a chunk was sent completely, the code of the slot will check if more chunks have to be written and start sending them. If no more chunks have to be sent, the client starts receiving the reply, which will also finish the chunked request correctly. If pipelining of multiple chunked requests is desired, beginSend() should be called with the completion flag set to true, which terminates the request body appropriately.

Secure Connections

Opening a secure HTTPS connection is fairly simple. HTTPS is built upon the Pt::Ssl module and all one has to do is to assign a Pt::Ssl::Context to the client as shown here:

Pt::Ssl::Context& ctx = ...;
client.setSecure(ctx);

After setSecure() has been called, the client will open secure connections. Further usage of the HTTP client API is exactly the same as in case of normal HTTP.

Authentication

A HTTP server might respond with a "401 Authorization Required" reply with a WWW-Authenticate header, indicating that authentication is required for the requested resource. The client has then to repeat the request with appropriate credentials in the authentication headers of the request. The Pt::Http::Authenticator supports user authentication for HTTP clients:

Pt::Http::Credential cred("john", "12345")
auth.setCredentials("some-realm", cred);

First, the user credentials have to be added for the realm in which they are valid. The credentials can be used by all authentication methods. Basic authentication is supported by default, but other Pt::Http::Authentication methods can be added using addAuthentication(). The following example demonstrates how the request is prepared for resubmission:

// the client just received a "401 Authorization Required"
Pt::Http::Client& client = ...;
bool isAuth = auth.authenticate(client.request(), client.reply());

The authenticate() function processes the initial reply, in which the server requested the client to authenticate itself. The previously rejected request is complemented, so it can be sent again. If authentication is not possible, for example, because no credentials are available, authenticate() will return false to indicate failure.