Hoppscotch Desktop 的 Relay 详解:基于 libcurl 的 HTTP 请求中继层实现
Relay 是 Hoppscotch 桌面端与 Agent 中负责“把一次 HTTP 请求真正发出去”的 Rust crate。它封装了自定义头、证书、代理、本地系统集成等浏览器 Webview 无法直接完成的网络能力,通过一个 Request → execute() → Response 的异步接口,外加 cancel(request_id) 的取消机制,将 libcurl 的全部传输能力暴露给前端。读完本篇,你将理解 Relay 的请求模型、安全/认证配置项、错误类型,以及它在源码中的线程模型与调用链。
定位与适用场景
根据 README,Relay 的定义是:
A HTTP request-response relay used by Hoppscotch Desktop and Hoppscotch Agent for more advanced request handling including custom headers, certificates, proxies, and local system integration.
也就是说,它是 Hoppscotch Desktop 与 Hoppscotch Agent 共用的请求中继层。浏览器环境对自定义 TLS 证书、代理认证、Digest 认证等支持有限,桌面端因此把这类“高级请求处理”下沉到原生侧,由 Relay 完成。crate 元信息见 Cargo.toml(name = "relay",版本 0.1.1)。
需要注意的分发限制(README 原文标注为 IMPORTANT):该 crate 目前只能通过 Git 获取,未发布到 crates.io。README 给出的安装方式是:
[dependencies]
relay = { git = "https://github.com/CuriousCorrelation/relay.git" }
README 列出的特性清单包括:基于 libcurl 的 HTTP 客户端;HTTP/1.1、HTTP/2、HTTP/3 支持;SSL/TLS 证书管理;带认证代理的支持;多种认证方式(Basic、Bearer、Digest);内容处理(JSON、Form Data、Binary);自定义安全配置;带取消支持的异步请求执行。
安装与运行环境要求
README 明确给出了运行前提(Requirements):
- Rust 1.77.2 或更高版本
- OpenSSL 开发库
- 带 SSL 与 HTTP/2 支持的 libcurl
其中有一个值得注意的工程细节(README 标注为 WARNING):Relay 对部分依赖使用了自定义 fork,目的有两个——NTLM 支持,以及跨平台一致的 OpenSSL 后端。这与 Cargo.toml 中的实际依赖完全对应:
curl = { git = "https://github.com/CuriousCorrelation/curl-rust.git", features = ["ntlm"] }
openssl = { version = "0.10.66", features = ["vendored"] }
# NOTE: This crate follows `openssl-sys` from https://github.com/CuriousCorrelation/curl-rust.git
# to avoid issues from version mismatch when compiling from source.
openssl-sys = { version = "0.9.64", features = ["vendored"] }
curl 依赖来自 CuriousCorrelation 维护的 curl-rust fork(开启 ntlm feature),而 openssl-sys 显式声明为 vendored 并跟随 fork 的版本,以避免从源码编译 libcurl 时出现 OpenSSL 版本不匹配问题。其余关键依赖还包括 tokio-util(用于取消令牌)、dashmap(并发请求表)、http/http-serde(标准类型与序列化)、thiserror(错误派生)、time、infer(内容类型推断)等,均可以在 Cargo.toml 中直接核对。
公开 API 与基本用法
crate 的对外面非常收敛。src/lib.rs 仅导出两个模块级入口与两个类型:
pub use interop::{Request, Response};
pub use relay::{cancel, execute};
即:构造 Request、await execute(request) 得到 Response、需要时 await cancel(request_id)。README 给出的用法示例:
use relay::{Request, Response, execute};
let request = Request {
id: 1,
url: "https://api.example.com".to_string(),
method: Method::Get,
version: Version::Http2,
// ... configure other options
};
let response = execute(request).await?;
这里 Method 与 Version 来自 http crate(Cargo.toml 中的 http = "1.1.0"),因此 HTTP 方法、协议版本都复用 Rust 生态的标准类型。README 同时强调:所有请求都是异步执行的,并可通过 cancel(request_id) 取消。
请求模型:Request 的完整字段
Request 与 Response 定义在 src/interop.rs,且都实现了 Serialize/Deserialize——这是桌面端与 Rust 侧跨进程传参(Tauri IPC 的 JSON)的基础。Request 的全部字段(interop.rs#L333-L347):
pub struct Request {
pub id: i64, // 请求标识,用于取消与响应关联
pub url: String,
pub method: Method, // http crate 的 Method,serde 走 http_serde
pub version: Version, // HTTP/1.1、2、3
pub headers: Option<HashMap<String, String>>,
pub params: Option<HashMap<String, String>>,
pub content: Option<ContentType>,
pub auth: Option<AuthType>,
pub security: Option<SecurityConfig>,
pub proxy: Option<ProxyConfig>,
pub meta: Option<RequestMeta>,
}
几个子模型值得展开:
请求体:ContentType
ContentType(interop.rs#L145-L182)是一个按 kind 标签区分的 tagged enum,对应 README 中“Content handling (JSON, Form Data, Binary)”的描述:
Text { content: String, media_type }Json { content: serde_json::Value, media_type }Xml { content: String, media_type }Form { content: FormData, media_type }Binary { content: Bytes, media_type, filename: Option<String> }Multipart { content: FormData, media_type }Urlencoded { content: String, media_type }
其中 FormData 为 Vec<(String, Vec<FormValue>)>,FormValue 进一步区分 Text 与 File { filename, content_type, data }(interop.rs#L128-L141),因此 multipart 上传可以混合文本字段与二进制文件。MediaType 则枚举了约 40 种常见 MIME 类型(文本、application、音频、视频、图像),并以 #[serde(other)] Other 兜底未知类型(interop.rs#L9-L126)。
行为选项:RequestOptions
RequestMeta.options 中的 RequestOptions(interop.rs#L321-L330):
| 字段 | 类型 | 作用 |
|---|---|---|
timeout |
Option<u64> |
请求超时,毫秒 |
follow_redirects |
Option<bool> |
是否跟随重定向 |
max_redirects |
Option<u32> |
最大重定向次数 |
decompress |
Option<bool> |
是否自动解压缩 |
cookies |
Option<bool> |
是否启用 cookie 处理 |
keep_alive |
Option<bool> |
TCP keep-alive |
这些选项在 src/request.rs 的 setup_basics() 中逐项落到 curl 句柄:timeout 转为 Duration::from_millis(request.rs#L148-L159);decompress = false 时把 Accept-Encoding 固定为 identity 以关闭自动解压(request.rs#L161-L172);cookies = true 时调用 cookie_file("") 启用 curl 的内存 cookie 引擎(request.rs#L174-L185)。默认情况下 accept_encoding("") 表示接受所有编码(request.rs#L106-L114)。
响应:Response 与元数据
Response(interop.rs#L356-L369)包含 id、status、statusText、version、headers、cookies(完整解析后的 Cookie 列表,含 domain/path/expires/sameSite)、body(Bytes + MediaType)以及 meta。ResponseMeta 携带两组统计(interop.rs#L405-L422):
TimingInfo { start, end }:请求时间窗口;SizeInfo { headers, body, total }:头部、正文与总字节数。
这与 README 示例“Blazingly fast”之外更实际的卖点一致:桌面端 UI 可以直接展示耗时与传输量。
认证:AuthType 的六种模式与支持边界
AuthType(interop.rs#L229-L277)覆盖 None、Basic、Bearer、Digest、ApiKey、OAuth2、Aws 七种形态(README 特性清单中列出 Basic/Bearer/Digest,源码实际还支持 API Key、OAuth2 与 AWS 签名)。其分派逻辑集中在 src/auth.rs 的 set_auth()(auth.rs#L20-L83),各模式的底层实现要点:
- Basic:直接
handle.username()/handle.password(),由 libcurl 处理(auth.rs#L85-L106)。 - Bearer:写入
Authorization: Bearer {token}头(auth.rs#L108-L112)。 - Digest:在 Basic 凭据之外,额外用
curl::easy::Auth开启digest(true)并http_auth(auth.rs#L146-L164)。Digest 的可选参数realm、nonce、algorithm(MD5/SHA-256/SHA-512)、qop(auth/auth-int)等定义在 interop.rs#L279-L292。 - ApiKey:
Header位置直接注入请求头;Query位置在auth.rs中留有注释,说明应在设置 URL 前拼入查询参数,该逻辑目前位于 request.rs 中一段被注释的 TODO 代码块——从源码结构看,API Key 的 query 模式尚处于迁移中,阅读代码时应留意这一点。 - AWS SigV4:
set_aws_auth目前只打印日志并直接返回Ok(())(auth.rs#L133-L144),注释表明“AWS SigV4 auth is handled at application level”,即签名在调用方(前端/Agent)完成,Relay 仅透传。 - OAuth2:这是 Relay 中唯一会发起“额外请求”的认证模式。
set_auth对 OAuth2 的三级回退策略(auth.rs#L62-L77)是:有access_token则按 Bearer 使用;只有refresh_token则走grant_type=refresh_token刷新;否则按GrantType发起授权流。GrantType(interop.rs#L193-L220)定义了四种:
| GrantType | 字段 | Relay 的支持情况 |
|---|---|---|
ClientCredentials |
token_endpoint、client_id、client_secret | 支持,直接 POST token endpoint |
Password |
token_endpoint、username、password | 支持,直接 POST token endpoint |
AuthorizationCode |
auth_endpoint、token_endpoint、client_id、client_secret | 不支持,返回 UnsupportedFeature |
Implicit |
auth_endpoint、client_id | 不支持,返回 UnsupportedFeature |
不支持的原因是源码中明确的:Authorization Code 与 Implicit 流程“requires browser interaction”(auth.rs#L184-L199)。令牌获取本身在 request_token() 中用一个独立的 Easy 句柄向 token endpoint 发送 URL 编码后的表单,并把 JSON 响应解析为 TokenResponse { access_token, token_type, expires_in, refresh_token, scope }(auth.rs#L263-L325),最后把拿到的 access_token 落回 Bearer 头。
安全配置:证书、校验与 CA
README 的 Security Features 一节给出的示例是:
let security_config = SecurityConfig {
validate_certificates: Some(true),
verify_host: Some(true),
certificates: Some(CertificateConfig {
client: Some(CertificateType::Pem {
cert: cert_data,
key: key_data
}),
ca: Some(vec![ca_cert_data])
})
};
对照实际源码,字段名略有差异:SecurityConfig(interop.rs#L301-L308)的字段是 certificates、verify_host(serde 名 verifyHost)、verify_peer(serde 名 verifyPeer),而非 README 示例中的 validate_certificates。以源码为准,Rust 侧可写成:
let security_config = SecurityConfig {
verify_peer: Some(true), // 对应 serde 字段 verifyPeer
verify_host: Some(true),
certificates: Some(CertificateConfig {
client: Some(CertificateType::Pem { cert: cert_data, key: key_data }),
ca: Some(vec![ca_cert_data]),
}),
};
CertificateType 支持 Pem { cert, key } 与 Pfx { data, password } 两种形态(interop.rs#L294-L299)。src/security.rs 中的 SecurityHandler::configure() 展示了这些字段如何映射到 libcurl:
verify_peer→handle.ssl_verify_peer(verify),失败时返回RelayError::Certificate(security.rs#L24-L33);verify_host→handle.ssl_verify_host(verify)(security.rs#L35-L44);- 客户端证书:PEM 走
ssl_cert_type("PEM")+ssl_cert_blob+ssl_key_type("PEM")+ssl_key_blob(security.rs#L76-L114);PFX(PKCS#12)则先用openssl::pkcs12::Pkcs12::from_der+parse2(password)解出证书与私钥,转成 PEM 后复用同一条路径(security.rs#L116-L158)——这正是依赖中引入opensslcrate 的直接原因; - CA 证书列表:逐个
ssl_cainfo_blob注入(security.rs#L160-L172)。
这套能力对应的正是桌面端“跳过证书校验 / 使用自签名 CA / mTLS 客户端证书”等高级网络设置。
代理配置
ProxyConfig { url, auth: Option<ProxyAuth> } 与 ProxyAuth { username, password } 定义在 interop.rs#L371-L381。应用侧逻辑在 CurlRequest::prepare() 的后半段(request.rs#L228-L262):先 handle.proxy(url),再把代理认证方式设为 Auth::new().auto(true),最后仅在用户名与密码都非空时写入 proxy_username / proxy_password。auto 让 libcurl 自行协商 HTTP Basic 或 NTLM——NTLM 能力正是来自前面提到的 curl-rust fork 的 ntlm feature。
执行与取消:源码级的线程模型
README 的 NOTE 说“所有请求异步执行且可取消”,src/relay.rs 给出了具体机制:
lazy_static::lazy_static! {
static ref ACTIVE_REQUESTS: DashMap<i64, Arc<AtomicBool>> = DashMap::new();
}
execute(request)是async fn,但真正耗时的 curl 传输是阻塞的,因此它在函数体内std::thread::spawn一个专用线程执行execute_request(),主协程通过handle.join()取回结果(relay.rs#L104-L155)。每个请求独立线程,避免了 libcurlEasy句柄的共享与借用问题,代价是每请求一个线程。- 取消路径:
execute入口先把请求的Arc<AtomicBool>注册进ACTIVE_REQUESTS(key 为request.id)。cancel(request_id)只需把对应布尔置真(relay.rs#L158-L175);若找不到该 id 则返回RelayError::Network { message: "Request not found" }。执行线程结束后检查取消标志,被取消的请求统一收敛为RelayError::Abort { message: "Request cancelled by user" }(relay.rs#L128-L139)。 - 单条请求的组装流程在
execute_request()(relay.rs#L27-L101):创建Easy句柄 →CurlRequest::prepare()完成方法/URL/版本/内容/认证/安全/代理配置 →TransferHandler挂接写回调并执行传输 → 读取response_code()与header_size()→ 交给ResponseHandler组装最终Response(含起止时间与大小统计)。全程使用tracing记录 method、url、status、body_size 等结构化日志,并固定开启verbose(true)与 debug 回调,把 libcurl 的握手/重定向细节输出到 trace 层。
值得说明的一点:CancellationToken 也被创建并传给 TransferHandler(relay.rs#L116-L126),但当前对用户的取消判定最终以 AtomicBool 为准;从源码结构看,取消是“协作式”的——传输线程完成后才收敛为 Abort 错误。
错误模型:RelayError
README 的 Error Handling 一节给出的简化版是:
#[derive(Error)]
pub enum RelayError {
Network { message: String, cause: Option<String> },
Certificate { message: String, cause: Option<String> },
Parse { message: String, cause: Option<String> },
// ... other variants
}
完整的 RelayError 定义在 src/error.rs,共有五个变体,且同样实现了 Serialize/Deserialize,方便跨 IPC 传递:
| 变体 | 含义 | 典型触发点 |
|---|---|---|
UnsupportedFeature { feature, message, relay } |
请求了当前 relay 不支持的特性 | OAuth2 的 Authorization Code / Implicit 流程、Implicit 刷新 |
Network { message, cause } |
网络/传输错误 | curl 句柄配置失败、线程 panic、“Request not found” |
Timeout { message, phase: Option<TimeoutPhase> } |
超时,且能指出阶段 | TimeoutPhase 细分 Connect(建连)、Tls(握手)、Response(等待响应) |
Certificate { message, cause } |
证书错误 | ssl_verify_peer 等安全配置失败、PKCS#12 解析失败 |
Parse { message, cause } |
响应解析失败 | 如 OAuth2 token 响应 JSON 解析失败 |
Abort { message } |
请求被中止 | cancel(request_id) 之后 |
此外 error.rs 还定义了 RequestResult<T>(Success { response } / Error { error } 的 tagged 枚举,error.rs#L63-L68),作为跨进程返回“要么成功要么错误”的统一封装。
桌面端如何调用 Relay
Relay 并非孤立 crate:Hoppscotch 前端通过 kernel 抽象访问它。packages/hoppscotch-desktop/src/kernel/relay.ts 展示了 TS 侧的封装形态:
export const Relay = (() => {
const module = () => getModule("relay")
return {
capabilities: () => module().capabilities,
canHandle: (request: RelayRequest): E.Either<RelayError, true> =>
module().canHandle(request),
execute: (request: RelayRequest) => module().execute(request),
} as const
})()
execute 的返回结构是 { cancel, emitter, response }——一个可取消句柄、一个事件发射器、以及一个 Promise<Either<RelayError, RelayResponse>>。这与 Rust 侧的 execute/cancel + tracing 事件流是一一对应的:前端的 cancel() 最终落到 Rust 的 cancel(request_id),而 Request/Response 的 serde 结构(camelCase 标签、tagged enum)保证了 JSON 边界的稳定序列化。结合 src/lib.rs 的导出面可以推断,canHandle/capabilities 属于上层 kernel 协议,Relay crate 本身只暴露 execute、cancel 与两个互操作类型。
小结
Relay 是 Hoppscotch Desktop 网络栈中“最后一公里”的 Rust 实现:以 Cargo.toml 中 fork 版 curl + vendored OpenSSL 为底座,用 interop.rs 中一组 serde 友好的 tagged 枚举把请求/响应建模成可 JSON 传输的契约;request.rs 负责把契约逐项映射为 libcurl 配置;auth.rs、security.rs 分别补齐认证与证书能力;relay.rs 用“每请求一线程 + DashMap 取消标志”提供异步与取消语义;error.rs 则把失败收敛为可跨进程传递的 RelayError。使用时需牢记三点前提:Rust ≥ 1.77.2、依赖通过 Git 引用(含 NTLM/NTLM 一致性所需的 fork)、OAuth2 的交互式授权流不在 Relay 支持范围内。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00