HTTP клиент на Rust: делаем запросы правильно
Rust — это современный язык системного программирования, который завоевал популярность благодаря своей производительности и безопасности памяти. Однако многие разработчики, привыкшие к Python или JavaScript, задаются вопросом: как в Rust выполнять HTTP-запросы? В этой статье мы разберём создание HTTP-клиента на Rust с использованием популярной библиотеки reqwest. Вы узнаете, как делать GET и POST запросы, обрабатывать JSON, управлять заголовками и работать с асинхронностью. После прочтения вы сможете уверенно интегрировать HTTP-клиент в свои Rust-проекты.
1. Установка и настройка окружения
Для начала создадим новый проект Rust и добавим необходимые зависимости. Откройте терминал и выполните:
cargo new http_client_rust
cd http_client_rust
Теперь отредактируйте файл Cargo.toml, добавив следующие зависимости:
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Разберём каждую библиотеку:
- reqwest — основной HTTP-клиент. Флаг
jsonдобавляет поддержку сериализации/десериализации JSON. - tokio — асинхронный рантайм, необходимый для работы reqwest в асинхронном режиме.
- serde и serde_json — для работы с JSON-структурами.
После добавления зависимостей выполните cargo build, чтобы скачать и скомпилировать все библиотеки.
2. Базовый GET-запрос
Начнём с простого GET-запроса к публичному API. Создадим структуру для ответа и выполним запрос к сервису JSONPlaceholder:
use reqwest;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Post {
userId: u32,
id: u32,
title: String,
body: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let url = "https://jsonplaceholder.typicode.com/posts/1";
let response = reqwest::get(url).await?;
let post: Post = response.json().await?;
println!("{:#?}", post);
Ok(())
}
Разберём код:
- Атрибут
#[derive(Debug, Deserialize)]автоматически генерирует трейты для вывода и десериализации. #[tokio::main]превращает функцию в асинхронную точку входа.reqwest::get()выполняет GET-запрос, а.awaitожидает его завершения.response.json()преобразует JSON-ответ в нашу структуру.
Запустите код командой cargo run. Вы увидите данные поста в консоли.
3. POST-запрос с отправкой данных
Теперь разберём, как отправлять данные на сервер. Для POST-запроса нам потребуется создать структуру для отправляемых данных и использовать метод post():
use reqwest::Client;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
struct NewPost {
title: String,
body: String,
userId: u32,
}
#[derive(Debug, Deserialize)]
struct CreatedPost {
id: u32,
title: String,
body: String,
userId: u32,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new();
let new_post = NewPost {
title: "Мой первый пост на Rust".to_string(),
body: "Это тело поста, отправленного с помощью reqwest".to_string(),
userId: 1,
};
let response = client
.post("https://jsonplaceholder.typicode.com/posts")
.json(&new_post)
.send()
.await?;
let created_post: CreatedPost = response.json().await?;
println!("Создан пост с ID: {}", created_post.id);
println!("{:#?}", created_post);
Ok(())
}
Ключевые моменты:
- Мы создаём экземпляр
Client, который можно переиспользовать для нескольких запросов. - Метод
.json()автоматически сериализует нашу структуру в JSON и устанавливает заголовокContent-Type: application/json. - Метод
.send()выполняет запрос, а затем мы десериализуем ответ.
4. Работа с заголовками и обработка ошибок
В реальных проектах часто требуется добавлять кастомные заголовки (например, для авторизации) и корректно обрабатывать ошибки. Рассмотрим пример с GitHub API:
use reqwest::Client;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct GitHubUser {
login: String,
id: u32,
name: Option<String>,
public_repos: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("my-rust-app"));
headers.insert(
AUTHORIZATION,
HeaderValue::from_str("Bearer YOUR_GITHUB_TOKEN")?
);
let response = client
.get("https://api.github.com/users/rust-lang")
.headers(headers)
.send()
.await?;
// Проверка статуса ответа
if response.status().is_success() {
let user: GitHubUser = response.json().await?;
println!("Пользователь: {}", user.login);
println!("Публичных репозиториев: {}", user.public_repos);
} else {
let status = response.status();
let error_text = response.text().await?;
eprintln!("Ошибка {}: {}", status, error_text);
}
Ok(())
}
Важные аспекты:
- HeaderMap позволяет собирать заголовки в коллекцию.
- Всегда устанавливайте заголовок
USER_AGENTпри работе с GitHub API — это требование их политики. - Проверка
response.status().is_success()— правильный способ обработки HTTP-статусов. - Тип возврата
Result<(), Box