Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

11.1 Network Client

Building network clients in Rust can range from low-level raw socket programming to high-level abstractions for common protocols like HTTP. The choice depends on the specific requirements for control, performance, and development speed.

For building HTTP clients, reqwest is a high-level crate that significantly simplifies the process of making HTTP requests. It abstracts away the complexities of underlying network protocols like TCP and TLS, as well as HTTP-specific details such as headers, redirects, and body encoding.

reqwest is built on top of hyper, a high-performance HTTP library, ensuring efficiency for demanding applications.

To use reqwest, the appropriate dependencies must be added to Cargo.toml.

Blocking Client:
A blocking network client performs operations (such as connecting, reading, or writing) by waiting until each operation is fully completed before moving on to the next, effectively halting program execution during network delays. In contrast, a non-blocking network client initiates these operations without waiting for them to finish, allowing the program to continue running or handle other tasks concurrently while waiting for the network to respond. This makes non-blocking clients more suitable for high-performance, responsive, or event-driven applications, though they require more complex control logic, such as polling, callbacks, or asynchronous programming.

For straightforward command-line tools or applications where synchronous operation is acceptable, reqwest provides a blocking API. The Cargo.toml file should contain:

[dependencies]
reqwest = { version = "0.12", features = ["blocking"] }

This configuration includes the blocking feature for reqwest, which allows for synchronous HTTP requests, making it simpler to use in non-asynchronous contexts or for quick scripts.

A practical example of a basic command-line web browser demonstrating an HTTP GET request is as follows:

Blocking Example: \

use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let url = "http://dcu.ie"; // Using HTTP for simplicity; HTTPS is covered later
println!("Attempting to fetch content from: {}", url);
// Send a GET request and immediately try to get the response body as text.
// The ? operator propagates any errors (e.g., network issues, invalid URL).
let response_body = reqwest::blocking::get(url)?.text()?;
println!("\n--- Page Content (Blocking) ---\n");
// Print the entire response body.
println!("{}", response_body);
Ok(())
}

This straightforward program uses reqwest::blocking::get to send a synchronous HTTP GET request to the specified URL. It then retrieves the response body as a String using .text() and prints it to the console. The ? operator provides concise error handling, propagating reqwest::Error or std::io::Error up the call stack as per the previous examples.

The output of this will depend on the page being presented on the day and is in raw html format. You will see something like:

</div>
</div>
</div>
</a>
<div class="profile-title">Tom Burke</div>
<div class="profile-group">Staff </div>
<div class="profile-body"><p dir="ltr">For his PhD, documentary maker Tom Burke turned the camera on his fellow filmmakers to explore the ethical
dilemmas involved in the relationship between the interviewer and interviewee.</p></div><br>
<a href="/community-profiles/tom-burke" aria-label="Tom Burke">Read more about Tom Burke</a></span></div></div>
<div class="card views-row"><div class="views-field views-field-title"><span class="field-content"><a href="/community-profiles/niamh-walsh" aria-label="Niamh Walsh">
<div class="profile-image">

Asynchronous (non-blocking) Client:
For building scalable web services or applications requiring high concurrency, reqwest integrates seamlessly with tokio, Rust’s leading asynchronous runtime.

The following dependencies should be present in the Cargo.toml file:

[package]
name = "hello_een1097"
version = "0.1.0"
edition = "2024"
[dependencies]
reqwest = "0.12" # Default features include async client
tokio = { version = "1", features = ["full"] } # "full" includes net, io, time, etc.

This setup enables reqwest to perform asynchronous HTTP requests, leveraging Tokio for non-blocking I/O, which is crucial for performance in concurrent applications.

Asynchronous Example (using Tokio):

use std::error::Error;
#[tokio::main] // This macro designates main as an asynchronous
// entry point, setting up the Tokio runtime.
async fn main() -> Result<(), Box<dyn Error>> {
let url = "http://example.com";
println!("Attempting to fetch content asynchronously from: {}", url);
// Send asynchronous GET request. .await pauses execution until response received.
let response = reqwest::get(url).await?;
// Retrieve the response body as text asynchronously.
let response_body = response.text().await?;
println!("\n--- Page Content (Asynchronous) ---\n");
println!("{}", response_body);
Ok(())
}

This gives the output:

Attempting to fetch content asynchronously from: http://example.com
--- Page Content (Asynchronous) ---
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

This version leverages Rust’s async/await syntax with #[tokio::main] to perform the HTTP GET request non-blockingly. The .await keyword is used to pause the execution of the current task until the network operation completes, allowing the Tokio runtime to execute other tasks concurrently. This demonstrates how reqwest can be effectively used in an asynchronous context for improved responsiveness and scalability.

The use of reqwest here, compared to directly using std::net::TcpStream for raw HTTP communication, illustrates a crucial aspect of Rust’s ecosystem. While it is technically possible to implement HTTP by manually crafting headers and parsing responses from raw TCP streams,** reqwest offers a significantly higher-level, more convenient abstraction. This highlights how powerful, opinionated crates dramatically boost developer productivity by abstracting away the intricate, low-level details of complex network protocols.** Instead of spending time on manually constructing HTTP request lines, headers, and parsing response bodies from raw TCP streams, reqwest allows developers to focus directly on the application’s core logic. This approach simplifies the learning curve for common networking tasks, enabling more time to be allocated to advanced topics or unique application requirements.

For scenarios where HTTP is not suitable, or where full, byte-level control over a custom protocol is required, direct interaction with TCP sockets using Rust’s standard library is necessary. This approach is fundamental for understanding the underlying mechanisms of network communication, especially on constrained-performance edge devices.

A practical example of a raw TCP client connecting to a local echo server is as follows:

use std::io::{self, Read, Write};
use std::net::TcpStream;
fn main() -> io::Result<()> {
println!("Attempting to connect to TCP echo server on tcpbin.com:4242..");
// Establish a TCP connection to the specified address.
// This call blocks until the connection is established or an error occurs.
let mut stream = TcpStream::connect("tcpbin.com:4242")?; // Connect to echo server
println!("Connected! Sending 'Hello, EEN1097 TCP!'");
// Send data to the server. write_all ensures that all bytes
// from the slice are written to the stream, handling internal retries.
stream.write_all(b"Hello, EEN1097 TCP!\n")?;
// flush ensures that any buffered data is immediately sent over the network.
stream.flush()?;
// Receive data from the server.
let mut buffer = [0; 512]; // A buffer to store incoming data.
// read attempts to read data into the buffer and returns the number of bytes read.
let bytes_read = stream.read(&mut buffer)?;
// Convert the received bytes into a human-readable string. See footnote
let received_data = String::from_utf8_lossy(&buffer[..bytes_read]);
println!("Received from server: {}", received_data);
Ok(())
}

On successful execution this code will result in:

Attempting to connect to TCP echo server on tcpbin.com:4242..
Connected! Sending 'Hello, EEN1097 TCP!'
Received from server: Hello, EEN1097 TCP!

This client program connects to a specified TCP address (tcpbin.com:4242), sends a message using write_all, and then reads the response into a buffer using read. The ? operator is used throughout for concise error handling, propagating any io::Error that might occur during network operations. This example provides a clear demonstration of direct, blocking TCP communication. We will build both ends of this communication shortly.

On edge networks, hosts can be unreachable or slow to respond. Without explicit timeouts, TcpStream::connect blocks indefinitely, stalling the entire program. Three methods address this:

MethodWhat it controls
TcpStream::connect_timeout(&addr, Duration)Maximum time to wait for the TCP handshake to complete
stream.set_read_timeout(Some(Duration))Maximum time a read() call waits before returning an error
stream.set_write_timeout(Some(Duration))Maximum time a write() call waits before returning an error

connect_timeout requires a pre-resolved SocketAddr rather than a string, because DNS resolution must happen first. Use to_socket_addrs() from the standard library for this:

use std::io::{self, Read, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
fn main() -> io::Result<()> {
// DNS resolution happens here — gives a SocketAddr needed by connect_timeout.
let addr = "tcpbin.com:4242"
.to_socket_addrs()?
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "hostname resolved to no address"))?;
// Fail after 5 seconds if the host cannot be reached.
let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
// Prevent a stalled read or write from hanging indefinitely.
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
stream.set_write_timeout(Some(Duration::from_secs(10)))?;
stream.write_all(b"Hello, timeout-aware TCP!\n")?;
stream.flush()?;
let mut buffer = [0; 512];
let bytes_read = stream.read(&mut buffer)?;
println!("Received: {}", String::from_utf8_lossy(&buffer[..bytes_read]));
Ok(())
}

When a read or write timeout fires, the operation returns Err with kind() equal to io::ErrorKind::TimedOut (Unix) or io::ErrorKind::WouldBlock (Windows). Check e.kind() before deciding whether to retry or abort.

Table 2: Summary of Networking alternatives

ToolKey FeatureWhat It IsWhen to Use It
reqwestAsyncA high-level, “batteries-included” HTTP client.Use this almost always for making web requests (calling APIs, downloading files). It handles HTTPS, JSON, etc., for you.
tokio::net::AsyncA mid-level asynchronous TCP/UDP socket library.Use this when you need high-performance, concurrent networking with a custom protocol (e.g., game server, chat app, database). Requires a runtime like Tokio.
std::net:BlockingA mid-level blocking TCP/UDP socket library.Use this only for simple scripts or learning purposes where you know you will only ever handle one connection at a time and don’t care about blocking.
TokioRuntimeThe asynchronous engine that runs all your async tasks.Use this as the foundation for any application that uses reqwest or tokio::net. You add #[tokio::main] to your main function to start the engine.
Concept Match

Match the Network Client Concepts

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

reqwest::blocking::get
drag a definition here…
#[tokio::main]
drag a definition here…
.await
drag a definition here…
connect_timeout
drag a definition here…
write_all
drag a definition here…

Definition Pool

Sends a synchronous HTTP GET request that blocks the calling thread until the full response is received.
Establishes a TCP connection to a pre-resolved SocketAddr, returning an error if the handshake does not complete within the specified duration.
Suspends the current async task until the awaited future resolves, returning control to the runtime so other tasks can execute in the meantime.
A method on Write that keeps writing until every byte of the provided slice has been sent, handling any partial writes the OS may return internally.
An attribute macro that transforms an async fn main() into a synchronous entry point by initialising the Tokio runtime automatically.
Quiz
Select 0/1

Why does TcpStream::connect_timeout require a SocketAddr rather than accepting a hostname string directly?

Quiz
Select 0/1

In the async reqwest example, what does calling reqwest::get(url).await? do?

Quiz
Select 0/1

What does String::from_utf8_lossy produce when the input byte slice contains an invalid UTF-8 sequence?