| name | reqwest |
| description | Rust reqwest HTTP 클라이언트 핵심 패턴 — GET/POST, JSON, 헤더, 스트리밍, 에러 처리, Client 재사용 |
reqwest HTTP 클라이언트
소스: https://docs.rs/reqwest/latest/reqwest/ | https://github.com/seanmonstar/reqwest
검증일: 2026-06-20
주의: 이 문서는 reqwest 0.12.x 기준으로 작성되었습니다. 0.13.x (최신 0.13.11, 2026-05-28 릴리즈)가 출시되어 Breaking Change가 있으므로 신규 프로젝트는 마이그레이션 노트를 참조하세요.
reqwest 0.13으로의 마이그레이션 시 주요 Breaking Change (0.12 → 0.13):
- rustls가 기본 TLS로 변경(aws-lc 기반),
rustls-tls feature가 rustls로 rename
- MSRV이 1.85로 상향
ClientBuilder::dns_resolver가 dns_resolver2로 교체
reqwest::Url의 serde Deserialize 지원이 별도 feature 필요
- 0.11.x 이하(hyper 0.14 기반) 대비 API 변경 있음
Cargo.toml 의존성
[dependencies]
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
json feature: .json() 메서드 활성화 (serde 연동)
stream feature: bytes_stream() 메서드 활성화 (스트리밍 응답)
Client 생성과 재사용
Client는 내부에 커넥션 풀을 유지한다. 요청마다 새로 만들지 않고 재사용해야 한다.
use reqwest::Client;
let client = Client::new();
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use std::time::Duration;
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, HeaderValue::from_str("Bearer sk-xxx")?);
let client = Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.pool_max_idle_per_host(10)
.build()?;
Client::new vs ClientBuilder:
Client::new(): 기본 설정, 빠른 프로토타이핑
Client::builder(): 타임아웃, 기본 헤더, TLS 설정 등 커스터마이징 필요 시
GET 요청
let body = client.get("https://httpbin.org/get")
.send()
.await?
.text()
.await?;
#[derive(serde::Deserialize)]
struct ApiResponse {
origin: String,
url: String,
}
let resp: ApiResponse = client.get("https://httpbin.org/get")
.send()
.await?
.json()
.await?;
POST 요청 (JSON)
#[derive(serde::Serialize)]
struct CreateRequest {
model: String,
max_tokens: u32,
messages: Vec<Message>,
}
#[derive(serde::Serialize)]
struct Message {
role: String,
content: String,
}
let request_body = CreateRequest {
model: "claude-sonnet-4-6".into(),
max_tokens: 1024,
messages: vec![Message {
role: "user".into(),
content: "Hello".into(),
}],
};
let response = client.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(&request_body)
.send()
.await?;
.json(&body) 호출 시 Content-Type: application/json 헤더가 자동으로 설정된다.
헤더 설정
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
let resp = client.post(url)
.header(AUTHORIZATION, format!("Bearer {}", token))
.header(CONTENT_TYPE, "application/json")
.header("x-api-key", api_key)
.send()
.await?;
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {}", token))?);
headers.insert("x-api-key", HeaderValue::from_str(api_key)?);
let resp = client.post(url)
.headers(headers)
.send()
.await?;
스트리밍 응답 처리 (SSE / bytes_stream)
Claude API의 Server-Sent Events 스트리밍 응답 처리 패턴.
use futures_util::StreamExt;
let response = client.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Hello"}]
}))
.send()
.await?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
let text = String::from_utf8_lossy(&chunk);
for line in text.lines() {
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
break;
}
let event: serde_json::Value = serde_json::from_str(data)?;
}
}
}
주의: SSE 청크가 이벤트 경계와 정확히 일치하지 않을 수 있다. 프로덕션에서는 버퍼링 로직 또는 eventsource-stream 같은 SSE 파서 크레이트 사용을 권장한다.
futures-util 의존성 필요:
futures-util = "0.3"
에러 처리
use reqwest::StatusCode;
#[derive(Debug)]
enum ApiError {
Network(reqwest::Error),
Status { code: StatusCode, body: String },
Parse(serde_json::Error),
}
impl From<reqwest::Error> for ApiError {
fn from(e: reqwest::Error) -> Self {
ApiError::Network(e)
}
}
async fn call_api(client: &Client, url: &str) -> Result<String, ApiError> {
let response = client.get(url).send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(ApiError::Status { code: status, body });
}
Ok(response.text().await?)
}
let resp = client.get(url)
.send()
.await?
.error_for_status()?;
reqwest::Error 주요 판별 메서드:
is_timeout() — 타임아웃 발생 여부
is_connect() — 연결 실패 여부
is_status() — HTTP 상태 코드 에러 여부
status() — Option<StatusCode> 반환
Claude API 호출 전체 예제
use reqwest::{Client, header::{HeaderMap, HeaderValue}};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Serialize)]
struct MessagesRequest {
model: String,
max_tokens: u32,
messages: Vec<Message>,
}
#[derive(Serialize, Deserialize)]
struct Message {
role: String,
content: String,
}
#[derive(Deserialize)]
struct MessagesResponse {
id: String,
content: Vec<ContentBlock>,
model: String,
stop_reason: Option<String>,
}
#[derive(Deserialize)]
struct ContentBlock {
#[serde(rename = "type")]
block_type: String,
text: Option<String>,
}
fn build_client(api_key: &str) -> reqwest::Result<Client> {
let mut headers = HeaderMap::new();
headers.insert("x-api-key", HeaderValue::from_str(api_key).unwrap());
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(60))
.build()
}
async fn send_message(
client: &Client,
model: &str,
user_message: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let body = MessagesRequest {
model: model.into(),
max_tokens: 1024,
messages: vec![Message {
role: "user".into(),
content: user_message.into(),
}],
};
let resp: MessagesResponse = client
.post("https://api.anthropic.com/v1/messages")
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp.content.into_iter()
.filter_map(|b| b.text)
.collect::<Vec<_>>()
.join(""))
}
재시도 패턴
reqwest 자체에는 재시도 기능이 없다. 직접 구현하거나 reqwest-middleware + reqwest-retry 크레이트 사용.
주의: reqwest 0.12.23(2025-08-08 릴리즈)부터 reqwest::retry 모듈과 ClientBuilder::retries(policy) 메서드가 내장됨. 단, 기본 내장 retry는 HTTP/2 REFUSED_STREAM 등 프로토콜 레벨 NACK 재시도 용도이며, 커스텀 정책이 필요한 경우 reqwest-middleware + reqwest-retry 조합이 더 유연함.
use std::time::Duration;
use tokio::time::sleep;
async fn retry_request(
client: &Client,
url: &str,
max_retries: u32,
) -> reqwest::Result<reqwest::Response> {
let mut last_err = None;
for attempt in 0..max_retries {
match client.get(url).send().await {
Ok(resp) if resp.status().is_server_error() => {
last_err = Some(resp.error_for_status().unwrap_err());
}
Ok(resp) => return Ok(resp),
Err(e) if e.is_timeout() || e.is_connect() => {
last_err = Some(e);
}
Err(e) => return Err(e),
}
let delay = Duration::from_millis(100 * 2u64.pow(attempt));
sleep(delay).await;
}
Err(last_err.unwrap())
}
주의: reqwest-middleware 0.4.x / reqwest-retry 0.7.x 기준. 버전 호환성 확인 필요.