About Reqwest
At Qxf2, we have always been curious to constantly learn and discover new possibilities and insights. This led us to explore the Reqwest crate, a standout tool for Rust, further enriching our technical toolkit. Reqwest is a client solution for Rust for making asynchronous HTTP requests. It provides API’s for interacting with various HTTP operations. It’s ease of use and its key features and capabilities make it a popular choice for handling HTTP communication. By default it supports HTTPS and ensures secure communication, handles timeouts and automatic retries. In this article, we will showcase few basic capabilities of reqwest library.
Our Application
For this post, we’ll utilize our Cars API Web Application specifically designed for our internal API Automation training. The application has endpoints for you to practice automating GET, POST, PUT and DELETE methods. In the following sections, we’ll use the GET (get cars list) and POST (add new car) requests on the Cars API using Reqwest crate.
Installation
To use Reqwest, you must include Reqwest and Tokio libraries in your Cargo.toml file. Tokio serves as the asynchronous runtime used under the hood of reqwest. Cargo will install them when you build your program.
[dependencies] reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1.12.0", features = ["full"] } serde = "1.0" serde_derive = "1.0" serde_json = "1.0" toml = "0.5.10" |
Making a GET request
Lets use a GET request to retrieve the cars data from the server. The provided code snippet demonstrates a simple Rust program that fetches information about cars from the server. The get_cars() function makes a GET request to the specified URL (https://cars-app.qxf2.com/cars) with basic authentication and retrieves the response body as json. The async/await
ensures continuous execution.
async fn get_cars(username: &str, password: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> { // Read the content of the "config.toml" file into a string let client = reqwest::Client::new(); let response = match client.get("https://cars-app.qxf2.com/cars") .basic_auth(username, Some(password)) .timeout(Duration::from_secs(10)) .send() .await { Ok(res) => { // Check for specific status codes if res.status().is_client_error() { Err(format!("Server returned a client error: {}", res.status()).into()) } else if res.status().is_server_error() { return Err(format!("Server returned a error, check your request data: {}", res.status()).into()); } else { Ok(res) } } Err(err) => { // Handle specific error types if err.is_connect() { return Err("Failed to connect to the server. Please make sure the server is running.".into()); } else if err.is_timeout() { return Err(format!("Request timed out: {}", err).into()); } else { return Err(Box::new(err)); } } }; // Handle the response and extract JSON let result_json: serde_json::Value = match response { Ok(res) => match res.json().await { Ok(json) => json, Err(err) => return Err(Box::new(err)), }, Err(err) => return Err(err), }; Ok(result_json) } |
This function uses a configuration file “config.toml” to store the username and password details. It performs an HTTP GET request with basic authentication and extracts the username and password from the Config struct. Upon a successful request (returning Ok(res)), we check for specific HTTP status codes in the response. We have implemented error-handling mechanism to manage various error scenarios and ensure that the system responds appropriately to different types of issues. Specifically, our error-handling includes
If reading the response text is successful (returns Ok(json)), store it in the body variable. If there is an error while reading the response (returns Err(err)), return the error wrapped in a Box.
The result looks below
Making a POST request
Moving on to the POST request, add_cars() function is responsible for adding cars to a server through an HTTP POST request. Basic authentication is applied, and the request includes a JSON payload (provided as the json_data parameter). The content type header is set to “application/json”, to indicate that the payload is in JSON format. The function returns a Result containing either the response body (if successful) or an error (if any step encountered an error). The error is wrapped in a Box
#[tokio::main] async fn add_cars(username: &str, password: &str, json_data: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> { let client = reqwest::Client::new(); let response = match client.post("https://cars-app.qxf2.com/cars/add") .basic_auth(username, Some(password)) .timeout(Duration::from_secs(10)) .header(reqwest::header::CONTENT_TYPE, "application/json") .body(json_data.to_string()) .send() .await { Ok(res) => { // Check for specific status codes if res.status().is_client_error() { return Err(format!("Server returned a client error: {}", res.status()).into()); } else if res.status().is_server_error() { return Err(format!("Server returned a error, check your request data: {}", res.status()).into()); } else { Ok(res) } } Err(err) => { // Handle specific error types if err.is_connect() { return Err("Failed to connect to the server. Please make sure the server is running.".into()); } else if err.is_timeout() { return Err(format!("Request timed out: {}", err).into()); } else { return Err(Box::new(err)); } } }; // Handle the response and extract JSON let result_json: serde_json::Value = match response { Ok(res) => match res.json().await { Ok(json) => json, Err(err) => return Err(Box::new(err)), }, Err(err) => return Err(err), }; Ok(result_json) } |
The output looks like this.
Here is the main.rs function illustrates the asynchronous execution of two functions: one for retrieving cars get_cars() and another for adding a new car add_cars(). The two helper functions
validate_get_response
Validates the JSON response from the GET request to ensure it conforms to the expected structure (CarsResponse), including checking for required fields in each car item.
validate_add_response
Validates the JSON response from the POST request for a “successful” field that indicates whether the addition was successful. You can find the complete code here
main.rs
//Rust script using reqwests to make Get and Post call to cars api to get and add cars use std::fs; use serde_derive::{Deserialize, Serialize}; use std::time::Duration; use serde_json::Value as JsonValue; #[derive(Debug, Deserialize)] struct ApiConfig { username: String, password: String, } #[derive(Debug, Deserialize)] struct Config { api: ApiConfig, } #[derive(Debug, Deserialize, Serialize)] struct Car { brand: String, car_type: String, name: String, price_range: String, } #[derive(Debug, Deserialize, Serialize)] struct CarsResponse { cars_list: Vec<Car>, successful: bool, } #[tokio::main] async fn main() { let config_str = fs::read_to_string("config.toml").expect("Failed to read config file"); // convert string into a Config struct let config: Config = toml::from_str(&config_str).expect("Failed to parse config"); // Read the username and password let username = &config.api.username; let password = &config.api.password; match get_cars(username, password).await { Ok(json_response) => { if validate_get_response(&json_response) { println!("Cars response is valid."); println!("Cars: {:#?}", json_response); } else { println!("Invalid cars response structure."); } } Err(err) => println!("Error: {}", err), } let json_data = r#" { "name": "Figo", "brand": "Ford", "price_range": "5-6lacs", "car_type": "hatchback" } "#; match add_cars(username, password, json_data).await { Ok(response) => { if validate_add_response(&response) { println!("Car addition successful."); println!("Response: {:#?}", response); } else { println!("Invalid car addition response structure."); } } Err(err) => println!("Error: {}", err), } } fn validate_get_response(cars_response: &JsonValue) -> bool { // Validate that the JSON response has the expected structure if let Ok(parsed_response) = serde_json::from_value::<CarsResponse>(cars_response.clone()) { // Check if each Car object in cars_list has all required fields for car in &parsed_response.cars_list { if car.brand.is_empty() || car.car_type.is_empty() || car.name.is_empty() || car.price_range.is_empty() { return false; } } true } else { false } } fn validate_add_response(response: &serde_json::Value) -> bool { // Check if the response contains the successful response if let Some(successful) = response.get("successful") { if let Some(successful_bool) = successful.as_bool() { return successful_bool; } } false // If the expected field or type is missing, consider the response as invalid } async fn get_cars(username: &str, password: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> { // Read the content of the "config.toml" file into a string let client = reqwest::Client::new(); let response = match client.get("http://localhost:5000/cars") .basic_auth(username, Some(password)) .timeout(Duration::from_secs(10)) .send() .await { Ok(res) => { // Check for specific status codes if res.status().is_client_error() { Err(format!("Server returned a client error: {}", res.status()).into()) } else if res.status().is_server_error() { return Err(format!("Server returned a error, check your request data: {}", res.status()).into()); } else { Ok(res) } } Err(err) => { // Handle specific error types if err.is_connect() { return Err("Failed to connect to the server. Please make sure the server is running.".into()); } else if err.is_timeout() { return Err(format!("Request timed out: {}", err).into()); } else { return Err(Box::new(err)); } } }; // Handle the response and extract JSON let result_json: serde_json::Value = match response { Ok(res) => match res.json().await { Ok(json) => json, Err(err) => return Err(Box::new(err)), }, Err(err) => return Err(err), }; Ok(result_json) } async fn add_cars(username: &str, password: &str, json_data: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> { let client = reqwest::Client::new(); let response = match client.post("http://localhost:5000/cars/add") .basic_auth(username, Some(password)) .timeout(Duration::from_secs(10)) .header(reqwest::header::CONTENT_TYPE, "application/json") .body(json_data.to_string()) .send() .await { Ok(res) => { // Check for specific status codes if res.status().is_client_error() { return Err(format!("Server returned a client error: {}", res.status()).into()); } else if res.status().is_server_error() { return Err(format!("Server returned a error, check your request data: {}", res.status()).into()); } else { Ok(res) } } Err(err) => { // Handle specific error types if err.is_connect() { return Err("Failed to connect to the server. Please make sure the server is running.".into()); } else if err.is_timeout() { return Err(format!("Request timed out: {}", err).into()); } else { return Err(Box::new(err)); } } }; // Handle the response and extract JSON let result_json: serde_json::Value = match response { Ok(res) => match res.json().await { Ok(json) => json, Err(err) => return Err(Box::new(err)), }, Err(err) => return Err(err), }; Ok(result_json) } |
The Reqwest crate is a powerful client solution for Rust designed to facilitate asynchronous HTTP requests. It supports HTTPS by default, ensuring secure communication, and includes features such as handling timeouts and automatic retries. In this article, we explored the application of Reqwest in the context of our Cars API Web Application and demonstrated the implementation of GET and POST requests in Rust, showcasing the ability to retrieve and add car information to the server. The code snippets illustrate how to handle errors, set up basic authentication, and structure requests with JSON payloads.
References
1. Reqwest Crate
2. Making HTTP requests in Rust with Reqwest
Hire testers from Qxf2
Hire technical testers from Qxf2. Our QA engineers go well beyond traditional test automation. We have a wealth of experience in testing critical parts of complex systems. We work well with small teams and early stage products. Get in touch with us to hire technical testers for your product.
I am an experienced engineer who has worked with top IT firms in India, gaining valuable expertise in software development and testing. My journey in QA began at Dell, where I focused on the manufacturing domain. This experience provided me with a strong foundation in quality assurance practices and processes.
I joined Qxf2 in 2016, where I continued to refine my skills, enhancing my proficiency in Python. I also expanded my skill set to include JavaScript, gaining hands-on experience and even build frameworks from scratch using TestCafe. Throughout my journey at Qxf2, I have had the opportunity to work on diverse technologies and platforms which includes working on powerful data validation framework like Great Expectations, AI tools like Whisper AI, and developed expertise in various web scraping techniques. I recently started exploring Rust. I enjoy working with variety of tools and sharing my experiences through blogging.
My interests are vegetable gardening using organic methods, listening to music and reading books.
One thought on “%1$s”