在 Rust 中使用 Reqwest 发起 HTTP 请求

更新于 2026-01-18

Ben Holmes 2021-11-02

得益于其强大的开源社区,Rust 似乎每天都在变得更加强大。Rust 已经成为全栈应用开发的一个可行选择(例如 Rocket 框架就非常有趣)。

但随着应用场景的无限扩展,我们也需要建立无限的数据连接。那么,我们该如何从外部获取数据呢?

一直把数据文件和命令行输入保存在本地机器上并不有趣。让我们学习一下 Rust 的 Reqwest 库如何:

  • 发起基本的 GET 和 POST 请求
  • 使用请求头(headers)进行身份验证并指定内容类型
  • 将 JSON 反序列化为可用的、类型安全的结构体
  • 帮助我们构建一个极简的 Spotify 搜索客户端

什么是 Rust?

如果你是 Rust 新手,它是一门对任何编程背景都非常友好的语言。我认为它是 C 语言的现代等价物。但为了使严格的类型系统和内存管理更易于处理,Rust 加入了许多人性化的设计,比如可读性极强的编译期错误提示,以及类似函数式编程中的 match 表达式等特性。

此外,Rust 拥有一个蓬勃发展的贡献者社区,以及大量优秀的教程资源。

什么是 Reqwest?

Reqwest 是一个用于通过 HTTP 协议获取资源的库。它既提供了简化版的 API 来对指定 URL 发起 getpost 请求,也提供了功能完整的 Client 模块,可用于设置请求头、Cookie、重定向策略等。

Reqwest 遵循 Rust 的异步协议,使用“future”机制。如果你还不熟悉 Rust 的异步编程,它包含两个关键要素:

  • 用于标记异步代码块的关键字(await 关键字)。如果你用过 JavaScript 中的 await,它在 Rust 中的工作方式类似。
  • 一个能识别这些关键字并高效执行异步程序的运行时环境。Rust 使用 async 关键字来标记需要等待响应的函数或代码块。不过,Reqwest 还依赖于 Tokio 运行时,以高效地调度异步事件。

若想深入了解,可以阅读 Rust 官方提供的《异步编程书》(The Async Book)。

使用 Reqwest 构建一个应用

免责声明:本示例基于 2021 年 10 月记录的 Spotify API。他们的 API 和认证协议此后可能已发生变化!但别担心,本示例中的核心知识点仍然适用。

在本项目中,我们将使用 Spotify API 构建一个简单的搜索客户端,以便快速获取歌曲链接并与朋友分享。为了简化流程,我们将跳过官方的完整认证流程。如果你想跟着操作,请前往 Spotify 开发者页面 获取一个可用于测试的 API token(注册账户是免费的)。

发起第一个 GET 请求(Reqwest)

让我们先尝试一些基本的 GET 和 POST 请求。首先,使用 Cargo 创建一个新项目,并在 Cargo.toml 中添加以下依赖项:

[dependencies]
reqwest = { version = "0.11", features = ["json"] } # 启用 JSON 解析支持的 reqwest
futures = "0.3" # 用于 async/await 代码块
tokio = { version = "1.12.0", features = ["full"] } # 用于异步运行时

现在,我们来调用 Spotify 的一个 API 端点:

use reqwest;

// tokio 允许我们在 main 函数上使用 "async"
#[tokio::main]
async fn main() {
    // 使用 .await 链式调用将得到查询结果
    let result = reqwest::get("https://api.spotify.com/v1/search").await;
}

非常简单!让我们打印看看结果:

println!("{:?}", result);

输出如下:

Ok(Response { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("api.spotify.com")), port: None, path: "/v1/search", query: None, fragment: None }, status: 401, headers: {"www-authenticate": "Bearer realm=\"spotify\"", "access-control-allow-origin": "*", "access-control-allow-headers": "Accept, App-Platform, Authorization...

嗯,我们收到的数据比预期多。为什么会这样?这是因为任何请求的初始响应都是服务器返回的原始 Response 对象。

如果你曾使用浏览器的开发者工具查看网络活动,你可能见过其中的一些字段。例如,status 字段对于匹配错误条件非常有用,这也引出了下一节的内容。

根据 StatusCode 进行匹配

你很可能希望根据响应状态码执行不同的逻辑。我们可以对请求的 StatusCode 进行模式匹配:

match response.status() {
    reqwest::StatusCode::OK => {
        println!("成功!{:?}");
    },
    reqwest::StatusCode::UNAUTHORIZED => {
        println!("需要获取一个新的 token");
    },
    _ => {
        panic!("哎呀!发生了意外情况。");
    },
}

当我们后续开始反序列化有效响应时,这种做法尤其有用,因为错误响应的 body 通常与正常响应的格式不同。

解析响应体

如果我们想要获取响应的实际内容(body),就需要再调用一个函数来指定解析方式。我们先尝试使用 .text() 方法将其作为纯文本处理:

#[tokio::main]
async fn main() {
    let response = reqwest::get("https://api.spotify.com/v1/search")
        .await
        // 每个响应都包装在 `Result` 类型中
        // 为简化起见,这里直接 unwrap
        .unwrap()
        .text()
        .await;
    println!("{:?}", response);
}

输出如下:

Ok("{\n  \"error\": {\n    \"status\": 401,\n    \"message\": \"No token provided\"\n  }\n}")

没什么可看的!因为我们没有提供身份验证 token,所以搜索请求返回了 401 错误,这应该会落入我们之前 match 语句中的 reqwest::StatusCode::UNAUTHORIZED 分支。

那么,如何才能获得 200 状态码的响应呢?接下来我们谈谈请求头(headers)。

使用请求头指定内容类型

我们需要在查询中添加几个请求头:

  • content_typeaccept:声明我们希望以 JSON 格式接收响应
  • authorization:传递我们的 Spotify 账户 token

我们不能直接将这些头信息传给 reqwest::get。因此,我们需要使用 Client 模块来链式添加这些头信息。让我们先重构之前的查询,改用 Client

#[tokio::main]
async fn main() {
    let client = reqwest::Client::new();
    let response = client
        .get("https://api.spotify.com/v1/search")
        // 使用 send() 确认发起请求
        .send()
        .await
        // 其余部分保持不变!
        .unwrap()
        .text()
        .await;
    println!("{:?}", response);
}

现在有了 client,我们可以添加头信息配置,如下所示:

let response = client
    .get("https://api.spotify.com/v1/search")
    .header(AUTHORIZATION, "Bearer [AUTH_TOKEN]")
    .header(CONTENT_TYPE, "application/json")
    .header(ACCEPT, "application/json")
    .send()
    ...

其中 [AUTH_TOKEN] 是你账户的 OAuth token(你可以在这里获取一个)。如果我们现在打印响应,应该会看到……一个不同的错误状态!

不再是 401 授权错误,而是 400 错误(Bad Request)。这是因为我们还没有指定要搜索的内容,所以现在来补充这一点:

let url = format!(
    "https://api.spotify.com/v1/search?q={query}&type=track,artist",
    // 去听听她最新的专辑,真的很🔥
    query = "Little Simz"
);

// 其余部分与之前相同!
let client = reqwest::Client::new();
let response = client
    .get(url)
    .header(AUTHORIZATION, "Bearer [AUTH_TOKEN]")
    .header(CONTENT_TYPE, "application/json")
    .header(ACCEPT, "application/json")
    .send()
    .await
    .unwrap();

println!("成功!{:?}", response);

输出如下:

Ok("{\n  \"artists\" : {\n    \"href\" : \"https://api.spotify.com/v1/search?query=Lil+Simz&type=artist&offset=0&limit=20\",\n    \"items\" : [ {\n      \"external_urls\"...

反序列化为 JSON

现在我们收到了一大串搜索结果。如何将其转换为有用的数据结构呢?

我们可以使用嵌套的结构体(struct)来建模我们期望接收的数据。具体结构因使用场景而异,但以下是 Spotify 搜索结果所需的模型:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct ExternalUrls {
    spotify: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Artist {
    name: String,
    external_urls: ExternalUrls,
}

#[derive(Serialize, Deserialize, Debug)]
struct Album {
    name: String,
    artists: Vec<Artist>,
    external_urls: ExternalUrls,
}

#[derive(Serialize, Deserialize, Debug)]
struct Track {
    name: String,
    href: String,
    popularity: u32,
    album: Album,
    external_urls: ExternalUrls,
}

#[derive(Serialize, Deserialize, Debug)]
struct Items<T> {
    items: Vec<T>,
}

#[derive(Serialize, Deserialize, Debug)]
struct APIResponse {
    tracks: Items<Track>,
}

注意:截至本文撰写时,Rust 不支持真正的“嵌套 struct”语法,因此我们将每一层拆分为独立命名且可序列化的 struct。

你还会注意到 Serialize 派生宏,它允许 Reqwest 通过 Serde 将原始 API 响应转换为 Rust 友好的类型。请确保将 Serde 添加为项目依赖项:

[dependencies]
...
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

现在我们准备好解析响应了。只有当 API 返回 200 状态时才应尝试解析,否则我们会遇到将错误消息解析为 APIResponse 结构体的问题。

让我们重新引入之前基于状态码的 match 语法:

let response = client
    .get(url)
    ...
match response.status() {
    reqwest::StatusCode::OK => {
        // 成功时,将 JSON 解析为 APIResponse
        match response.json::<APIResponse>().await {
            Ok(parsed) => println!("成功!{:?}", parsed),
            Err(_) => println!("嗯,响应格式与我们预期的不一致。"),
        };
    }
    reqwest::StatusCode::UNAUTHORIZED => {
        println!("需要获取一个新的 token");
    }
    other => {
        panic!("哎呀!发生了意外情况:{:?}", other);
    }
}

如果一切顺利,我们应该会得到一个可以使用的漂亮 API 输出。我们还通过在结构体中限定字段名,过滤掉了 API 响应中不感兴趣的键。

如果你习惯于 JavaScript 或 TypeScript,其中对象可能包含意外的键,那么 Rust 的这种严格性应该会让你感到安心!

创建一个 CLI 客户端

现在,让我们把这些数据整理成人类可读的格式。

我们将对歌曲列表进行一个简单的映射处理:

fn print_tracks(tracks: Vec<&Track>) {
    for track in tracks {
        println!("🔥 {}", track.name);
        println!("💿 {}", track.album.name);
        println!(
            "🕺 {}",
            track
                .album
                .artists
                .iter()
                .map(|artist| artist.name.to_string())
                .collect::<String>()
        );
        println!("🌎 {}", track.external_urls.spotify);
        println!("---------")
    }
}

然后在 match 语句中调用这个打印函数:

match response.status() {
    reqwest::StatusCode::OK => {
        match response.json::<APIResponse>().await {
            Ok(parsed) => print_tracks(parsed.tracks.items.iter().collect()),
            Err(_) => println!("嗯,响应格式与我们预期的不一致。"),
        };
    }
    ...
}

我们还应该接受任意的命令行参数作为搜索关键词:

use std::env;

let args: Vec<String> = env::args().collect();
let search_query = &args[1];
let url = format!(
    "https://api.spotify.com/v1/search?q={query}&type=track,artist",
    query = search_query
);
...

现在,我们就可以在终端中运行如下命令来获取搜索结果:

cargo run "Little Simz"

输出示例:

     Running `target/debug/spotify-search`
🔥 Venom
💿 Venom
🕺 Little Simz
🌎 https://open.spotify.com/track/4WaaWczlVb1UJ24LILsR4C
---------
🔥 Fear No Man
💿 Sometimes I Might Be Introvert
🕺 Little Simz
🌎 https://open.spotify.com/track/6bLkNijhsnr1MWYrO6XnRz
---------
🔥 Venom
💿 GREY Area
🕺 Little Simz
🌎 https://open.spotify.com/track/3A0ITFj6kbb9CggwtPe55f
---------
...

大功告成!

结论

希望本教程展示了 Reqwest 库的强大与简洁。如果你想进一步深入学习,Reqwest 的官方文档提供了许多有价值的示例,包括:

  • 设置 Cookie
  • 探索其他 HTTP 方法,如 POST 和 PUT
  • 提供更多辅助函数,用于判断状态码及其类别

如果你想查看完整项目代码,可以在 GitHub 上找到这个 Spotify 搜索客户端