O ecossistema Rust oferece diversas bibliotecas para realizar requisições HTTP, e reqwest é, sem dúvida, a mais popular e completa. Ela fornece uma API ergonômica e assíncrona que se integra perfeitamente ao modelo de concorrência do Rust, permitindo construir clientes HTTP de alto desempenho. Nesta aula, exploraremos os principais recursos do reqwest, desde os conceitos básicos de GET e POST até o tratamento de erros e a manipulação de JSON, passando pela programação assíncrona que é fundamental para o uso eficiente da biblioteca.

Dominar o reqwest é essencial para qualquer desenvolvedor Rust que precise interagir com serviços web, seja consumindo APIs REST, enviando dados para servidores ou baixando conteúdo. Vamos começar entendendo como configurar o projeto e fazer nossas primeiras requisições.

GET e POST

As operações mais básicas em qualquer cliente HTTP são as requisições GET e POST. O GET é utilizado para recuperar dados de um servidor, enquanto o POST envia dados para criar ou modificar recursos. O reqwest fornece métodos dedicados para cada verbo HTTP, tornando o código claro e direto.

Para começar, você precisa adicionar a dependência reqwest ao seu Cargo.toml. Como vamos usar a funcionalidade assíncrona, também precisaremos de um runtime, como o tokio. Vamos configurar o projeto:

[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Agora, vejamos um exemplo de requisição GET simples. Vamos buscar dados de uma API pública (JSONPlaceholder) e imprimir o corpo da resposta:

use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://jsonplaceholder.typicode.com/posts/1";
    let response = reqwest::get(url).await?;
    let body = response.text().await?;
    println!("Resposta: {}", body);
    Ok(())
}

Para uma requisição POST, precisamos enviar dados no corpo da mensagem. O reqwest permite enviar dados em vários formatos, como JSON, formulário ou texto puro. Aqui está um exemplo de POST com JSON:

use reqwest::Client;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let url = "https://jsonplaceholder.typicode.com/posts";
    let payload = json!({
        "title": "Meu post",
        "body": "Conteúdo do post",
        "userId": 1
    });
    let response = client.post(url)
        .json(&payload)
        .send()
        .await?;
    let status = response.status();
    let body = response.text().await?;
    println!("Status: {}", status);
    println!("Resposta: {}", body);
    Ok(())
}

Perceba que usamos Client::new() para criar um cliente reutilizável, o que é mais eficiente do que criar uma nova conexão a cada requisição. O método .json() serializa automaticamente o payload para JSON e define o header Content-Type apropriadamente.

Async

O reqwest é totalmente assíncrono, o que significa que as operações de rede não bloqueiam a thread atual. Isso é crucial para aplicações que precisam lidar com muitas requisições simultâneas ou que têm interfaces de usuário responsivas. Em Rust, a programação assíncrona é baseada em async/await e em um runtime como o Tokio.

Para usar o reqwest, você precisa estar dentro de um contexto assíncrono (geralmente uma função async). O runtime é responsável por agendar as tarefas e gerenciar as operações de I/O. Utilizamos a macro #[tokio::main] para transformar nossa função main em uma função assíncrona executada pelo Tokio.

Vamos explorar como fazer múltiplas requisições concorrentes usando tokio::join! ou tokio::spawn. Isso demonstra o poder do async: enquanto uma requisição espera a resposta, outras podem ser processadas. Exemplo:

use reqwest::Client;
use serde_json::Value;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let url1 = "https://jsonplaceholder.typicode.com/posts/1";
    let url2 = "https://jsonplaceholder.typicode.com/posts/2";

    let (resp1, resp2) = tokio::join!(
        client.get(url1).send(),
        client.get(url2).send()
    );

    let body1 = resp1?.text().await?;
    let body2 = resp2?.text().await?;
    println!("Resposta 1: {}", body1);
    println!("Resposta 2: {}", body2);
    Ok(())
}

Neste exemplo, as duas requisições são enviadas simultaneamente e aguardamos ambas com tokio::join!. Isso reduz o tempo total de espera em comparação a fazer requisições sequenciais.

É importante lembrar que o reqwest também suporta o modo bloqueante (síncrono) através do recurso blocking, mas isso deve ser evitado em aplicações de alto desempenho. Prefira sempre a API assíncrona.

JSON

Trabalhar com JSON é uma necessidade comum em clientes HTTP. O reqwest integra-se perfeitamente com a biblioteca serde para serialização e desserialização de dados. Com o recurso json habilitado, você pode enviar e receber JSON de forma transparente.

Para desserializar uma resposta JSON em uma estrutura de dados, definimos um struct com os campos desejados e derivamos os traits Deserialize e Serialize. Vamos criar um exemplo com a API JSONPlaceholder, que retorna um post:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Post {
    userId: u32,
    id: u32,
    title: String,
    body: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "https://jsonplaceholder.typicode.com/posts/1";
    let post: Post = reqwest::get(url).await?.json().await?;
    println!("Título: {}", post.title);
    println!("Corpo: {}", post.body);
    Ok(())
}

O método .json() lê o corpo da resposta e tenta desserializar para o tipo especificado. Se a estrutura do JSON não corresponder ao struct, ocorrerá um erro.

Para serializar dados ao enviar uma requisição, usamos .json() no builder do request, como já vimos. O reqwest também permite trabalhar com serde_json::Value para JSON dinâmico, quando você não quer definir uma estrutura fixa. Exemplo:

use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let response = client.post("https://httpbin.org/post")
        .json(&json!({"nome": "João", "idade": 30}))
        .send()
        .await?;
    let json: Value = response.json().await?;
    println!("{:#?}", json);
    Ok(())
}

O tipo Value é útil quando a estrutura do JSON é desconhecida ou variável. Você pode acessar os campos com indexação, por exemplo, json["nome"].

Tratamento de erros

Em aplicações reais, erros são inevitáveis: rede instável, servidor retornando status 404 ou 500, JSON malformado, etc. O reqwest fornece um sistema de erro robusto através do tipo reqwest::Error, que encapsula todos os erros possíveis durante a requisição.

Quando você usa o operador ? em uma função que retorna Result, os erros são propagados automaticamente. No entanto, é importante tratar erros de forma mais granular. Podemos verificar o status da resposta e agir de acordo. Exemplo:

use reqwest::{Client, StatusCode};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let response = client.get("https://httpbin.org/status/404").send().await?;

    match response.status() {
        StatusCode::OK => {
            let body = response.text().await?;
            println!("Sucesso: {}", body);
        }
        StatusCode::NOT_FOUND => {
            eprintln!("Recurso não encontrado (404)");
        }
        status => {
            eprintln!("Erro inesperado: {}", status);
        }
    }
    Ok(())
}

Além disso, o reqwest oferece o método .error_for_status() que transforma respostas com status 4xx ou 5xx em um erro. Isso simplifica o tratamento quando você espera que o servidor retorne sucesso:

let response = client.get(url).send().await?.error_for_status()?;

Também podemos usar match para tratar diferentes tipos de erro, como falha de conexão, timeout ou erro de serialização. O tipo reqwest::Error possui métodos como is_timeout(), is_connect(), etc. Exemplo:

match client.get(url).send().await {
    Ok(response) => {
        // processar resposta
    }
    Err(e) => {
        if e.is_timeout() {
            eprintln!("Tempo esgotado");
        } else if e.is_connect() {
            eprintln!("Falha de conexão");
        } else {
            eprintln!("Erro: {}", e);
        }
    }
}

Para erros de desserialização JSON, o erro virá do método .json() e será do tipo serde_json::Error. Você pode usar map_err para converter em um tipo de erro unificado. Em aplicações maiores, é comum definir um enum de erro personalizado que abranja todos os casos.

Boas práticas e observações finais

Ao trabalhar com reqwest, é importante considerar alguns pontos:

  • Reutilize o cliente: Crie um único Client e compartilhe-o, em vez de criar um novo a cada requisição. Isso economiza recursos de conexão.
  • Configure timeouts: Use Client::builder() para definir timeouts de conexão e de resposta, evitando que sua aplicação fique travada indefinidamente.
  • Trate erros de forma explícita: Não ignore erros. Use error_for_status() e verifique os códigos de status quando necessário.
  • Prefira async: A API assíncrona é mais escalável e adequada para aplicações modernas. Evite o modo bloqueante a menos que seja estritamente necessário.
  • Use tipos fortes com serde: Defina structs para os dados que você espera receber, em vez de trabalhar com Value o tempo todo. Isso traz segurança de tipos e facilita a manutenção.

Com esses conhecimentos, você está pronto para integrar serviços HTTP em seus projetos Rust de forma eficiente e robusta.

Referências

Exercícios

  1. Exercício 1: Escreva uma função assíncrona que faça uma requisição GET para https://jsonplaceholder.typicode.com/posts/1 e imprima o título do post. Use a struct Post definida na aula.
  2. ✓ Resposta:
    use reqwest;
    use serde::Deserialize;
    
    #[derive(Debug, Deserialize)]
    struct Post {
        userId: u32,
        id: u32,
        title: String,
        body: String,
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let url = "https://jsonplaceholder.typicode.com/posts/1";
        let post: Post = reqwest::get(url).await?.json().await?;
        println!("Título: {}", post.title);
        Ok(())
    }
  3. Exercício 2: Crie uma função que envie uma requisição POST para https://jsonplaceholder.typicode.com/posts com um JSON contendo title, body e userId (use 1). Imprima o status e o corpo da resposta.
  4. ✓ Resposta:
    use reqwest::Client;
    use serde_json::json;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let client = Client::new();
        let url = "https://jsonplaceholder.typicode.com/posts";
        let payload = json!({
            "title": "Meu post",
            "body": "Conteúdo do post",
            "userId": 1
        });
        let response = client.post(url)
            .json(&payload)
            .send()
            .await?;
        println!("Status: {}", response.status());
        let body = response.text().await?;
        println!("Corpo: {}", body);
        Ok(())
    }
  5. Exercício 3: Faça duas requisições GET concorrentes usando tokio::join! para buscar os posts 1 e 2 de https://jsonplaceholder.typicode.com/posts/. Imprima o título de cada um.
  6. ✓ Resposta:
    use reqwest::Client;
    use serde::Deserialize;
    
    #[derive(Debug, Deserialize)]
    struct Post {
        title: String,
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let client = Client::new();
        let url1 = "https://jsonplaceholder.typicode.com/posts/1";
        let url2 = "https://jsonplaceholder.typicode.com/posts/2";
    
        let (resp1, resp2) = tokio::join!(
            client.get(url1).send(),
            client.get(url2).send()
        );
    
        let post1: Post = resp1?.json().await?;
        let post2: Post = resp2?.json().await?;
        println!("Post 1: {}", post1.title);
        println!("Post 2: {}", post2.title);
        Ok(())
    }
  7. Exercício 4: Escreva um código que faça uma requisição GET para https://httpbin.org/status/404 e trate o erro verificando o status. Se for 404, imprima "Recurso não encontrado". Caso contrário, imprima o status.
  8. ✓ Resposta:
    use reqwest::{Client, StatusCode};
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let client = Client::new();
        let response = client.get("https://httpbin.org/status/404").send().await?;
        match response.status() {
            StatusCode::NOT_FOUND => println!("Recurso não encontrado"),
            status => println!("Status: {}", status),
        }
        Ok(())
    }
  9. Exercício 5: Faça uma requisição POST para https://httpbin.org/post com um JSON contendo uma chave "mensagem" e o valor "Olá, mundo!". Use serde_json::Value para extrair a chave "json" da resposta e imprima o valor da mensagem.
  10. ✓ Resposta:
    use reqwest::Client;
    use serde_json::{json, Value};
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        let client = Client::new();
        let response = client.post("https://httpbin.org/post")
            .json(&json!({"mensagem": "Olá, mundo!"}))
            .send()
            .await?;
        let json: Value = response.json().await?;
        let mensagem = json["json"]["mensagem"].as_str().unwrap_or("");
        println!("Mensagem: {}", mensagem);
        Ok(())
    }