HTTP Client Tutorial
This tutorial builds a simple HTTP client that connects to a server, sends a GET request, and reads the response. You’ll learn socket connection, composed I/O operations, and the exception-based error handling pattern.
|
Code snippets assume:
|
Overview
Making an HTTP request involves:
-
Creating and opening a socket
-
Connecting to the server
-
Sending the HTTP request
-
Reading the response
-
Handling connection close (EOF)
We’ll use the exception-based pattern, throwing std::system_error on
failure, for concise code.
Building the Request
HTTP/1.1 requests have a simple text format:
std::string build_request(std::string_view host)
{
return "GET / HTTP/1.1\r\n"
"Host: " + std::string(host) + "\r\n"
"Connection: close\r\n"
"\r\n";
}
The Connection: close header tells the server to close the connection
after sending the response. This simplifies our code because we know EOF
marks the end of the response.
The Request Coroutine
capy::task<void> do_request(
corosio::io_stream& stream,
std::string_view host)
{
// Build and send the request
std::string request = build_request(host);
if (auto [ec, n] = co_await capy::write(
stream, capy::const_buffer(request.data(), request.size())); ec)
throw std::system_error(ec);
// Read the entire response
std::string response;
auto [ec, n] = co_await capy::read(
stream, capy::string_dynamic_buffer(&response));
// Reading into a dynamic buffer completes with success at EOF
if (ec && ec != capy::cond::eof)
throw std::system_error(ec);
std::cout << response << std::endl;
}
Key points:
-
The write throws if writing fails
-
capy::read(stream, capy::string_dynamic_buffer(&response))reads until EOF -
Reading into a dynamic buffer completes with success at end-of-stream — it returns no error and reports the total bytes read. The
!= capy::cond::eofcheck is a harmless defensive guard, not the thing that signals end-of-response
The Connection Coroutine
capy::task<void> run_client(
corosio::io_context& ioc,
corosio::ipv4_address addr,
std::uint16_t port)
{
corosio::tcp_socket s(ioc);
s.open();
// Connect (throws on error)
if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec)
throw std::system_error(ec);
co_await do_request(s, addr.to_string());
}
The socket must be opened before connecting. We pass the socket as an
io_stream& to do_request, so the same function works with any plain
socket. TLS streams have a different type and need their own overload, as
shown below.
Main Function
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cerr << "Usage: http_client <ip-address> <port>\n"
<< "Example: http_client 35.190.118.110 80\n";
return 1;
}
// Parse IP address
corosio::ipv4_address addr;
if (auto ec = corosio::parse_ipv4_address(argv[1], addr); ec)
{
std::cerr << "Invalid IP address: " << argv[1] << "\n";
return 1;
}
auto port = static_cast<std::uint16_t>(std::atoi(argv[2]));
corosio::io_context ioc;
capy::run_async(ioc.get_executor())(
run_client(ioc, addr, port));
ioc.run();
}
Reading Until EOF
Wrapping a std::string in capy::string_dynamic_buffer lets
capy::read grow it as data arrives, reading until EOF:
std::string response;
auto [ec, n] = co_await capy::read(
stream, capy::string_dynamic_buffer(&response));
This:
-
Automatically grows the string as needed
-
Completes with success (no error) when the connection closes, since the dynamic-buffer read treats EOF as the natural end of the stream
-
Returns the total bytes read in
n
Error vs. Exception Patterns
This example uses exceptions because:
-
Connection errors are fatal—we want to abort
-
The code is more linear without error checks
Compare structured bindings:
auto [ec] = co_await s.connect(ep);
if (ec)
{
std::cerr << "Connect failed: " << ec.message() << "\n";
co_return;
}
With exceptions:
if (auto [ec] = co_await s.connect(ep); ec) // Throw on error
throw std::system_error(ec);
Both are valid. Use exceptions when errors are exceptional; use structured bindings when errors are expected (like EOF during reading).
Running the Client
First, find an IP address for a website:
$ nslookup www.example.com
...
Address: 93.184.215.14
Then run the client:
$ ./http_client 93.184.215.14 80
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
...
<!doctype html>
<html>
...
</html>
Adding TLS Support
To make HTTPS requests, wrap the connected socket in a wolfssl_stream.
A wolfssl_stream is not an io_stream, so it needs its own
do_request overload taking corosio::tls_stream&:
#include <boost/corosio/wolfssl_stream.hpp>
capy::task<void> do_request(
corosio::tls_stream& stream,
std::string_view host)
{
std::string request = build_request(host);
if (auto [ec, n] = co_await capy::write(
stream, capy::const_buffer(request.data(), request.size())); ec)
throw std::system_error(ec);
std::string response;
auto [ec, n] = co_await capy::read(
stream, capy::string_dynamic_buffer(&response));
// As with the plain stream, the dynamic-buffer read completes with
// success at EOF; this check is a defensive guard.
if (ec && ec != capy::cond::eof)
throw std::system_error(ec);
std::cout << response << std::endl;
}
capy::task<void> run_https_client(
corosio::io_context& ioc,
corosio::ipv4_address addr,
std::uint16_t port,
std::string_view hostname)
{
corosio::tcp_socket s(ioc);
s.open();
if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec)
throw std::system_error(ec);
// Configure the TLS context
corosio::tls_context ctx;
ctx.set_hostname(hostname);
if (auto ec = ctx.set_default_verify_paths(); ec)
throw std::system_error(ec);
if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec)
throw std::system_error(ec);
// Wrap the connected socket without taking ownership (pointer form)
corosio::wolfssl_stream secure(&s, ctx);
if (auto [ec] = co_await secure.handshake(
corosio::wolfssl_stream::client); ec)
throw std::system_error(ec);
co_await do_request(secure, hostname);
if (auto [ec] = co_await secure.shutdown(); ec)
throw std::system_error(ec);
}
The TLS overload mirrors the plain one: capy::read and capy::write
work with tls_stream exactly as they do with io_stream. Only the
parameter type and the surrounding handshake/shutdown differ.
Next Steps
-
DNS Lookup — Resolve hostnames to addresses
-
TLS Guide — WolfSSL integration details
-
Composed Operations — How read/write work