Rust: выполнение HTTP-запросов и обработка ответов с помощью reqwest

В Rust HTTP-запросы и парсинг результата выполнять легко  —  нужны лишь подходящие библиотеки. reqwest и serde могут стать идеальным решением.

Репозиторий на GitHub

Весь код приложения доступен в репозитории GitHub.

Вы научитесь:

  • выполнять запросы GET и POST;
  • отображать HTTP-ответ на предопределенную структуру;
  • обрабатывать различные коды состояния HTTP.

Зависимости

Чтобы установить зависимости для следующей сборки, добавим библиотеки reqwest, tokio, serde и serde_json в файл Cargo.toml:

[package]
name = "example_make_http_request"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] } # для асинхронной среды выполнения
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Общая настройка и импортирование

Прежде чем переходить к коду бизнес-логики, рассмотрим код вокруг нее.

В следующем фрагменте импортируются:

  • структура HashMap для JSONResponse;
  • 2 типажа из serde для преобразования HTTP-ответов в структуры *Response;
  • CONTENT_TYPE из крейта reqwest для установки заголовка запроса content-type (типа содержимого).

А в методе main инициализируется новый клиент reqwest  —  один для всех последующих запросов:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use reqwest::header::CONTENT_TYPE;


#[derive(Serialize, Deserialize, Debug)]
struct GETAPIResponse {
origin: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct JSONResponse {
json: HashMap<String, String>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Создается новый клиент, используемый во всех запросах
let client = reqwest::Client::new();

/// Бизнес-логика помещается сюда
/// ...
/// ...

Ok(())
}

GET-запрос

В следующем фрагменте кода, используя в этом client метод .get(url), создаем GET-запрос для отправки по .send(). Поскольку возвращаться должен JSON, задаем тип содержимого application/json.

По завершении запроса (.await?) ответ в формате JSON десериализуем в структуру GETAPIResponse, используя метод .json::<GETAPIResponse>():

//...


// Выполняется GET-запрос,
// а также парсинг ответа в структуру GETAPIResponse
let resp200 = client.get("https://httpbin.org/ip")
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json::<GETAPIResponse>()
.await?;

println!("{:#?}", resp200);
// Вывод:
/*
GETAPIResponse {
origin: "182.190.14.159",
}
*/

//...

Примечание. Возвращаемый JSON не обязан точно соответствовать структуре GETAPIResponse. Обязательно лишь поле origin, представленное строкой. Другие поля не важны.

POST-запрос

Создается так же, но с двумя отличиями.

  1. .post(url) вместо .get(url).
  2. В теле запроса передается дополнительная полезная нагрузка.

В следующем примере создается изменяемая HashMap и добавляются 2 пары «ключ  —  значение». В Rust HashMap сериализуется методом .json(&T), в случае успеха сериализованные данные добавляются в тело запроса.

Здесь парсинг ответа выполняется в структуру JSONResponse с единственным полем json. В этом поле содержится HashMap<String, String>. Поскольку тело запроса возвращается из конечной точки https://httpbin.org/anything в поле json тела ответа, оно идеально десериализуется в структуру JSONResponse:

// Создается карта со строковыми парами «ключ — значение» 
// — полезной нагрузкой тела запроса
let mut map = HashMap::new();
map.insert("lang", "rust");
map.insert("body", "json");


// Выполняется POST-запрос,
// а также парсинг ответа в структуру JSONResponse
let resp_json = client.post("https://httpbin.org/anything")
.json(&map)
.send()
.await?
.json::<JSONResponse>()
.await?;

println!("{:#?}", resp_json);
// Вывод:
/*
JSONResponse {
json: {
"body": "json",
"lang": "rust",
},
}
*/

Обработка различных кодов состояния HTTP

Не все запросы возвращаются с 200 OK и десериализуются в структуры.

В следующем примере делается GET-запрос к https://httpbin.org/status/404. Последней частью (status/404) URL-адреса указывается, что ответы сервера всегда должны быть с кодом 404, чтобы проверять сопоставитель.

Логика всех кодов состояния реализуется с помощью match resp404.status(), для всего остального есть ветвь по умолчанию с символом _. В примере ниже выполняется сопоставление с ветвью reqwest::StatusCode::NOT_FOUND:

// Делается GET-запрос
let resp404 = client.get("https://httpbin.org/status/404")
.send()
.await?;

// Сопоставляется код состояния HTTP запроса
match resp404.status() {
// "OK - 200" — все прошло хорошо
reqwest::StatusCode::OK => {
println!("Success!");
// ...
},
// "NOT_FOUND - 404" — ресурс не найден
reqwest::StatusCode::NOT_FOUND => {
println!("Got 404! Haven't found resource!");
// Вывод:
/*
Ошибка 404! Ресурс не найден!
*/
},
// Любой другой код состояния, не совпадающий с приведенными выше
_ => {
panic!("Okay... this shouldn't happen...");
},
};

Запуск приложения

Загляните в репозиторий GitHub.

А теперь компилируем и запускаем приложение на cargo run. Вы увидите такой вывод, пути и origin будут другими:

Запуск `cargo run`

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Pascal Zwikirsch: Rust: Making HTTP Requests And Handling Responses by Using reqwest

Предыдущая статья3 основных закона разработки ПО
Следующая статьяКак упростить автоматизированное тестирование компонентов React