Введение: зачем писать HTTP клиент на Rust?
Современная разработка редко обходится без взаимодействия с внешними сервисами. Будь то REST API погоды, база данных или микросервисная архитектура — умение отправлять HTTP-запросы является базовым навыком. Rust, с его акцентом на производительность и безопасность, предлагает несколько элегантных решений для работы с HTTP. В этой статье мы разберем, как правильно создавать HTTP клиенты на Rust: от выбора библиотеки до обработки сложных сценариев.
Мы рассмотрим две самые популярные библиотеки: reqwest (асинхронный стандарт) и ureq (простой синхронный подход). Вы узнаете, как отправлять GET и POST запросы, обрабатывать JSON, управлять заголовками и избегать типичных ошибок.
1. Выбор библиотеки: reqwest vs ureq
Прежде чем писать код, нужно определиться с инструментом. В экосистеме Rust есть два основных кандидата.
1.1. Reqwest — мощный и асинхронный
reqwest — это де-факто стандарт для HTTP-запросов в Rust. Он построен на базе hyper и tokio, поддерживает асинхронность, автоматическую сериализацию/десериализацию JSON и работу с cookies. Идеален для высоконагруженных приложений и веб-серверов.
Для начала работы добавьте в Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
1.2. Ureq — простота без асинхронности
Если вам не нужны асинхронные вызовы и вы пишете простой скрипт или CLI-утилиту, ureq станет отличным выбором. Он синхронный, минималистичный и не требует сложной настройки рантайма.
[dependencies]
ureq = "2.9"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Когда что выбирать? Для веб-серверов и сложных приложений — reqwest. Для простых задач, тестов и скриптов — ureq.
2. Базовые запросы: GET и POST
Рассмотрим практические примеры для обеих библиотек.
2.1. GET запрос с reqwest
Самый простой пример — получение данных с публичного API. Мы используем асинхронную функцию main с макросом #[tokio::main].
use reqwest;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let url = "https://jsonplaceholder.typicode.com/posts/1";
let response = reqwest::get(url).await?;
// Проверяем статус ответа
if response.status().is_success() {
let body = response.text().await?;
println!("Ответ: {}", body);
} else {
eprintln!("Ошибка HTTP: {}", response.status());
}
Ok(())
}
2.2. POST запрос с JSON телом (reqwest)
Отправка данных часто требует сериализации структуры. Для этого используем крейт serde.
use reqwest;
use serde::{Deserialize, Serialize};
use std::error::Error;
#[derive(Serialize, Deserialize, Debug)]
struct Post {
title: String,
body: String,
user_id: i32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
let new_post = Post {
title: "Hello Rust".to_string(),
body: "Это тело запроса".to_string(),
user_id: 1,
};
let response = client
.post("https://jsonplaceholder.typicode.com/posts")
.json(&new_post)
.send()
.await?;
let created_post: Post = response.json().await?;
println!("Созданный пост: {:?}", created_post);
Ok(())
}
2.3. Простой GET с ureq
С ureq код становится короче, так как не требует асинхронности:
use ureq;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let response = ureq::get("https://api.github.com/repos/rust-lang/rust")
.call()?;
let body: String = response.into_string()?;
println!("Ответ: {}", body);
Ok(())
}
3. Продвинутые техники: заголовки, таймауты и обработка ошибок
В реальных проектах простых запросов недостаточно. Рассмотрим, как настроить HTTP клиент правильно.
3.1. Настройка заголовков и User-Agent
Многие API требуют указания User-Agent или авторизационного токена. В reqwest это делается через билдер клиента:
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("MyRustApp/1.0"));
headers.insert(
AUTHORIZATION,
HeaderValue::from_static("Bearer your_token_here"),
);
let client = reqwest::Client::builder()
.default_headers(headers)
.timeout(std::time::Duration::from_secs(10))
.build()?;
let response = client
.get("https://api.example.com/data")
.send()
.await?;
println!("Статус: {}", response.status());
Ok(())
}
3.2. Обработка ошибок и повторные попытки
Сеть нестабильна, поэтому важно корректно обрабатывать ошибки. Используйте паттерн retry с помощью крейта backoff или простого цикла:
use reqwest;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut attempts = 0;
let max_retries = 3;
loop {
attempts += 1;
let result = client
.get("https://httpbin.org/status/500") // Симулируем ошибку