
Hoppscotch Desktop 的 Relay 详解基于 libcurl 的 HTTP 请求中继层实现【免费下载链接】hoppscotchOpen-Source API Development Ecosystem • https://hoppscotch.io • Offline, On-Prem Cloud • Web, Desktop CLI • Open-Source Alternative to Postman, Insomnia项目地址: https://gitcode.com/GitHub_Trending/ho/hoppscotchRelay 是 Hoppscotch 桌面端与 Agent 中负责“把一次 HTTP 请求真正发出去”的 Rust crate。它封装了自定义头、证书、代理、本地系统集成等浏览器 Webview 无法直接完成的网络能力通过一个Request → execute() → Response的异步接口外加cancel(request_id)的取消机制将 libcurl 的全部传输能力暴露给前端。读完本篇你将理解 Relay 的请求模型、安全/认证配置项、错误类型以及它在源码中的线程模型与调用链。定位与适用场景根据 READMERelay 的定义是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.tomlname 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 明确给出了运行前提RequirementsRust 1.77.2 或更高版本OpenSSL 开发库带 SSL 与 HTTP/2 支持的 libcurl其中有一个值得注意的工程细节README 标注为 WARNINGRelay 对部分依赖使用了自定义 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开启ntlmfeature而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来自httpcrateCargo.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-L347pub struct Request { pub id: i64, // 请求标识用于取消与响应关联 pub url: String, pub method: Method, // http crate 的 Methodserde 走 http_serde pub version: Version, // HTTP/1.1、2、3 pub headers: OptionHashMapString, String, pub params: OptionHashMapString, String, pub content: OptionContentType, pub auth: OptionAuthType, pub security: OptionSecurityConfig, pub proxy: OptionProxyConfig, pub meta: OptionRequestMeta, }几个子模型值得展开请求体ContentTypeContentTypeinterop.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: OptionString }Multipart { content: FormData, media_type }Urlencoded { content: String, media_type }其中FormData为Vec(String, VecFormValue)FormValue进一步区分Text与File { filename, content_type, data }interop.rs#L128-L141因此 multipart 上传可以混合文本字段与二进制文件。MediaType则枚举了约 40 种常见 MIME 类型文本、application、音频、视频、图像并以#[serde(other)] Other兜底未知类型interop.rs#L9-L126。行为选项RequestOptionsRequestMeta.options中的RequestOptionsinterop.rs#L321-L330字段类型作用timeoutOptionu64请求超时毫秒follow_redirectsOptionbool是否跟随重定向max_redirectsOptionu32最大重定向次数decompressOptionbool是否自动解压缩cookiesOptionbool是否启用 cookie 处理keep_aliveOptionboolTCP keep-alive这些选项在 src/request.rs 的setup_basics()中逐项落到 curl 句柄timeout转为Duration::from_millisrequest.rs#L148-L159decompress false时把Accept-Encoding固定为identity以关闭自动解压request.rs#L161-L172cookies true时调用cookie_file()启用 curl 的内存 cookie 引擎request.rs#L174-L185。默认情况下accept_encoding()表示接受所有编码request.rs#L106-L114。响应Response 与元数据Responseinterop.rs#L356-L369包含id、status、statusText、version、headers、cookies完整解析后的Cookie列表含 domain/path/expires/sameSite、bodyBytesMediaType以及meta。ResponseMeta携带两组统计interop.rs#L405-L422TimingInfo { start, end }请求时间窗口SizeInfo { headers, body, total }头部、正文与总字节数。这与 README 示例“Blazingly fast”之外更实际的卖点一致桌面端 UI 可以直接展示耗时与传输量。认证AuthType 的六种模式与支持边界AuthTypeinterop.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_authauth.rs#L146-L164。Digest 的可选参数realm、nonce、algorithmMD5/SHA-256/SHA-512、qopauth/auth-int等定义在 interop.rs#L279-L292。ApiKeyHeader位置直接注入请求头Query位置在auth.rs中留有注释说明应在设置 URL 前拼入查询参数该逻辑目前位于 request.rs 中一段被注释的 TODO 代码块——从源码结构看API Key 的 query 模式尚处于迁移中阅读代码时应留意这一点。AWS SigV4set_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_typerefresh_token刷新否则按GrantType发起授权流。GrantTypeinterop.rs#L193-L220定义了四种GrantType字段Relay 的支持情况ClientCredentialstoken_endpoint、client_id、client_secret支持直接 POST token endpointPasswordtoken_endpoint、username、password支持直接 POST token endpointAuthorizationCodeauth_endpoint、token_endpoint、client_id、client_secret不支持返回UnsupportedFeatureImplicitauth_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 头。安全配置证书、校验与 CAREADME 的 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]) }) };对照实际源码字段名略有差异SecurityConfiginterop.rs#L301-L308的字段是certificates、verify_hostserde 名verifyHost、verify_peerserde 名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()展示了这些字段如何映射到 libcurlverify_peer→handle.ssl_verify_peer(verify)失败时返回RelayError::Certificatesecurity.rs#L24-L33verify_host→handle.ssl_verify_host(verify)security.rs#L35-L44客户端证书PEM 走ssl_cert_type(PEM)ssl_cert_blobssl_key_type(PEM)ssl_key_blobsecurity.rs#L76-L114PFXPKCS#12则先用openssl::pkcs12::Pkcs12::from_derparse2(password)解出证书与私钥转成 PEM 后复用同一条路径security.rs#L116-L158——这正是依赖中引入opensslcrate 的直接原因CA 证书列表逐个ssl_cainfo_blob注入security.rs#L160-L172。这套能力对应的正是桌面端“跳过证书校验 / 使用自签名 CA / mTLS 客户端证书”等高级网络设置。代理配置ProxyConfig { url, auth: OptionProxyAuth }与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 的ntlmfeature。执行与取消源码级的线程模型README 的 NOTE 说“所有请求异步执行且可取消”src/relay.rs 给出了具体机制lazy_static::lazy_static! { static ref ACTIVE_REQUESTS: DashMapi64, ArcAtomicBool DashMap::new(); }execute(request)是async fn但真正耗时的 curl 传输是阻塞的因此它在函数体内std::thread::spawn一个专用线程执行execute_request()主协程通过handle.join()取回结果relay.rs#L104-L155。每个请求独立线程避免了 libcurlEasy句柄的共享与借用问题代价是每请求一个线程。取消路径execute入口先把请求的ArcAtomicBool注册进ACTIVE_REQUESTSkey 为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也被创建并传给TransferHandlerrelay.rs#L116-L126但当前对用户的取消判定最终以AtomicBool为准从源码结构看取消是“协作式”的——传输线程完成后才收敛为Abort错误。错误模型RelayErrorREADME 的 Error Handling 一节给出的简化版是#[derive(Error)] pub enum RelayError { Network { message: String, cause: OptionString }, Certificate { message: String, cause: OptionString }, Parse { message: String, cause: OptionString }, // ... 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: OptionTimeoutPhase }超时且能指出阶段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还定义了RequestResultTSuccess { response }/Error { error }的 tagged 枚举error.rs#L63-L68作为跨进程返回“要么成功要么错误”的统一封装。桌面端如何调用 RelayRelay 并非孤立 crateHoppscotch 前端通过 kernel 抽象访问它。packages/hoppscotch-desktop/src/kernel/relay.ts 展示了 TS 侧的封装形态export const Relay (() { const module () getModule(relay) return { capabilities: () module().capabilities, canHandle: (request: RelayRequest): E.EitherRelayError, true module().canHandle(request), execute: (request: RelayRequest) module().execute(request), } as const })()execute的返回结构是{ cancel, emitter, response }——一个可取消句柄、一个事件发射器、以及一个PromiseEitherRelayError, RelayResponse。这与 Rust 侧的execute/canceltracing事件流是一一对应的前端的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 支持范围内。【免费下载链接】hoppscotchOpen-Source API Development Ecosystem • https://hoppscotch.io • Offline, On-Prem Cloud • Web, Desktop CLI • Open-Source Alternative to Postman, Insomnia项目地址: https://gitcode.com/GitHub_Trending/ho/hoppscotch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考